mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
fix(lsp): move sloppy import resolution from loader to resolver (#23751)
Moves sloppy import resolution from the loader to the resolver. Also adds some test helper functions to make the lsp tests less verbose --------- Co-authored-by: David Sherret <dsherret@gmail.com>
This commit is contained in:
parent
263b6b971d
commit
dc29986ae5
5 changed files with 283 additions and 287 deletions
|
@ -13,9 +13,7 @@ use super::tsc::AssetDocument;
|
||||||
use crate::cache::HttpCache;
|
use crate::cache::HttpCache;
|
||||||
use crate::graph_util::CliJsrUrlProvider;
|
use crate::graph_util::CliJsrUrlProvider;
|
||||||
use crate::lsp::logging::lsp_warn;
|
use crate::lsp::logging::lsp_warn;
|
||||||
use crate::resolver::SloppyImportsFsEntry;
|
use deno_graph::source::Resolver;
|
||||||
use crate::resolver::SloppyImportsResolution;
|
|
||||||
use crate::resolver::SloppyImportsResolver;
|
|
||||||
use deno_runtime::fs_util::specifier_to_file_path;
|
use deno_runtime::fs_util::specifier_to_file_path;
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
@ -390,7 +388,7 @@ impl Document {
|
||||||
d.with_new_resolver(
|
d.with_new_resolver(
|
||||||
s,
|
s,
|
||||||
&CliJsrUrlProvider,
|
&CliJsrUrlProvider,
|
||||||
Some(graph_resolver),
|
Some(&graph_resolver),
|
||||||
Some(npm_resolver),
|
Some(npm_resolver),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -400,7 +398,7 @@ impl Document {
|
||||||
maybe_types_dependency = self.maybe_types_dependency.as_ref().map(|d| {
|
maybe_types_dependency = self.maybe_types_dependency.as_ref().map(|d| {
|
||||||
Arc::new(d.with_new_resolver(
|
Arc::new(d.with_new_resolver(
|
||||||
&CliJsrUrlProvider,
|
&CliJsrUrlProvider,
|
||||||
Some(graph_resolver),
|
Some(&graph_resolver),
|
||||||
Some(npm_resolver),
|
Some(npm_resolver),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
@ -854,8 +852,6 @@ pub struct Documents {
|
||||||
/// Gets if any document had a node: specifier such that a @types/node package
|
/// Gets if any document had a node: specifier such that a @types/node package
|
||||||
/// should be injected.
|
/// should be injected.
|
||||||
has_injected_types_node_package: bool,
|
has_injected_types_node_package: bool,
|
||||||
/// If --unstable-sloppy-imports is enabled.
|
|
||||||
unstable_sloppy_imports: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Documents {
|
impl Documents {
|
||||||
|
@ -869,7 +865,6 @@ impl Documents {
|
||||||
resolver: Default::default(),
|
resolver: Default::default(),
|
||||||
npm_specifier_reqs: Default::default(),
|
npm_specifier_reqs: Default::default(),
|
||||||
has_injected_types_node_package: false,
|
has_injected_types_node_package: false,
|
||||||
unstable_sloppy_imports: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -996,54 +991,17 @@ impl Documents {
|
||||||
&self,
|
&self,
|
||||||
specifier: &ModuleSpecifier,
|
specifier: &ModuleSpecifier,
|
||||||
) -> Option<ModuleSpecifier> {
|
) -> Option<ModuleSpecifier> {
|
||||||
if self.unstable_sloppy_imports && specifier.scheme() == "file" {
|
let specifier = if let Ok(jsr_req_ref) =
|
||||||
Some(
|
JsrPackageReqReference::from_specifier(specifier)
|
||||||
self
|
{
|
||||||
.resolve_unstable_sloppy_import(specifier)
|
Cow::Owned(self.resolver.jsr_to_registry_url(&jsr_req_ref)?)
|
||||||
.into_specifier()
|
|
||||||
.into_owned(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
let specifier = if let Ok(jsr_req_ref) =
|
Cow::Borrowed(specifier)
|
||||||
JsrPackageReqReference::from_specifier(specifier)
|
};
|
||||||
{
|
if !DOCUMENT_SCHEMES.contains(&specifier.scheme()) {
|
||||||
Cow::Owned(self.resolver.jsr_to_registry_url(&jsr_req_ref)?)
|
return None;
|
||||||
} else {
|
|
||||||
Cow::Borrowed(specifier)
|
|
||||||
};
|
|
||||||
if !DOCUMENT_SCHEMES.contains(&specifier.scheme()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.resolver.resolve_redirects(&specifier)
|
|
||||||
}
|
}
|
||||||
}
|
self.resolver.resolve_redirects(&specifier)
|
||||||
|
|
||||||
fn resolve_unstable_sloppy_import<'a>(
|
|
||||||
&self,
|
|
||||||
specifier: &'a ModuleSpecifier,
|
|
||||||
) -> SloppyImportsResolution<'a> {
|
|
||||||
SloppyImportsResolver::resolve_with_stat_sync(
|
|
||||||
specifier,
|
|
||||||
ResolutionMode::Types,
|
|
||||||
|path| {
|
|
||||||
if let Ok(specifier) = ModuleSpecifier::from_file_path(path) {
|
|
||||||
if self.open_docs.contains_key(&specifier)
|
|
||||||
|| self.cache.contains(&specifier)
|
|
||||||
{
|
|
||||||
return Some(SloppyImportsFsEntry::File);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
path.metadata().ok().and_then(|m| {
|
|
||||||
if m.is_file() {
|
|
||||||
Some(SloppyImportsFsEntry::File)
|
|
||||||
} else if m.is_dir() {
|
|
||||||
Some(SloppyImportsFsEntry::Dir)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the specifier can be resolved to a document.
|
/// Return `true` if the specifier can be resolved to a document.
|
||||||
|
@ -1226,12 +1184,7 @@ impl Documents {
|
||||||
) {
|
) {
|
||||||
self.config = Arc::new(config.clone());
|
self.config = Arc::new(config.clone());
|
||||||
self.cache = cache;
|
self.cache = cache;
|
||||||
let config_data = config.tree.root_data();
|
|
||||||
let config_file = config_data.and_then(|d| d.config_file.as_deref());
|
|
||||||
self.resolver = resolver.clone();
|
self.resolver = resolver.clone();
|
||||||
self.unstable_sloppy_imports = config_file
|
|
||||||
.map(|c| c.has_unstable("sloppy-imports"))
|
|
||||||
.unwrap_or(false);
|
|
||||||
{
|
{
|
||||||
let fs_docs = &self.file_system_docs;
|
let fs_docs = &self.file_system_docs;
|
||||||
// Clean up non-existent documents.
|
// Clean up non-existent documents.
|
||||||
|
@ -1404,7 +1357,6 @@ fn node_resolve_npm_req_ref(
|
||||||
pub struct OpenDocumentsGraphLoader<'a> {
|
pub struct OpenDocumentsGraphLoader<'a> {
|
||||||
pub inner_loader: &'a mut dyn deno_graph::source::Loader,
|
pub inner_loader: &'a mut dyn deno_graph::source::Loader,
|
||||||
pub open_docs: &'a HashMap<ModuleSpecifier, Arc<Document>>,
|
pub open_docs: &'a HashMap<ModuleSpecifier, Arc<Document>>,
|
||||||
pub unstable_sloppy_imports: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> OpenDocumentsGraphLoader<'a> {
|
impl<'a> OpenDocumentsGraphLoader<'a> {
|
||||||
|
@ -1426,32 +1378,6 @@ impl<'a> OpenDocumentsGraphLoader<'a> {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_unstable_sloppy_import<'b>(
|
|
||||||
&self,
|
|
||||||
specifier: &'b ModuleSpecifier,
|
|
||||||
) -> SloppyImportsResolution<'b> {
|
|
||||||
SloppyImportsResolver::resolve_with_stat_sync(
|
|
||||||
specifier,
|
|
||||||
ResolutionMode::Types,
|
|
||||||
|path| {
|
|
||||||
if let Ok(specifier) = ModuleSpecifier::from_file_path(path) {
|
|
||||||
if self.open_docs.contains_key(&specifier) {
|
|
||||||
return Some(SloppyImportsFsEntry::File);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
path.metadata().ok().and_then(|m| {
|
|
||||||
if m.is_file() {
|
|
||||||
Some(SloppyImportsFsEntry::File)
|
|
||||||
} else if m.is_dir() {
|
|
||||||
Some(SloppyImportsFsEntry::Dir)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> deno_graph::source::Loader for OpenDocumentsGraphLoader<'a> {
|
impl<'a> deno_graph::source::Loader for OpenDocumentsGraphLoader<'a> {
|
||||||
|
@ -1460,17 +1386,9 @@ impl<'a> deno_graph::source::Loader for OpenDocumentsGraphLoader<'a> {
|
||||||
specifier: &ModuleSpecifier,
|
specifier: &ModuleSpecifier,
|
||||||
options: deno_graph::source::LoadOptions,
|
options: deno_graph::source::LoadOptions,
|
||||||
) -> deno_graph::source::LoadFuture {
|
) -> deno_graph::source::LoadFuture {
|
||||||
let specifier = if self.unstable_sloppy_imports {
|
match self.load_from_docs(specifier) {
|
||||||
self
|
|
||||||
.resolve_unstable_sloppy_import(specifier)
|
|
||||||
.into_specifier()
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed(specifier)
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.load_from_docs(&specifier) {
|
|
||||||
Some(fut) => fut,
|
Some(fut) => fut,
|
||||||
None => self.inner_loader.load(&specifier, options),
|
None => self.inner_loader.load(specifier, options),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1531,7 +1449,7 @@ fn analyze_module(
|
||||||
// dynamic imports like import(`./dir/${something}`) in the LSP
|
// dynamic imports like import(`./dir/${something}`) in the LSP
|
||||||
file_system: &deno_graph::source::NullFileSystem,
|
file_system: &deno_graph::source::NullFileSystem,
|
||||||
jsr_url_provider: &CliJsrUrlProvider,
|
jsr_url_provider: &CliJsrUrlProvider,
|
||||||
maybe_resolver: Some(resolver.as_graph_resolver()),
|
maybe_resolver: Some(&resolver.as_graph_resolver()),
|
||||||
maybe_npm_resolver: Some(resolver.as_graph_npm_resolver()),
|
maybe_npm_resolver: Some(resolver.as_graph_npm_resolver()),
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
|
|
|
@ -251,7 +251,6 @@ impl LanguageServer {
|
||||||
let mut loader = crate::lsp::documents::OpenDocumentsGraphLoader {
|
let mut loader = crate::lsp::documents::OpenDocumentsGraphLoader {
|
||||||
inner_loader: &mut inner_loader,
|
inner_loader: &mut inner_loader,
|
||||||
open_docs: &open_docs,
|
open_docs: &open_docs,
|
||||||
unstable_sloppy_imports: cli_options.unstable_sloppy_imports(),
|
|
||||||
};
|
};
|
||||||
let graph = module_graph_creator
|
let graph = module_graph_creator
|
||||||
.create_graph_with_loader(GraphKind::All, roots.clone(), &mut loader)
|
.create_graph_with_loader(GraphKind::All, roots.clone(), &mut loader)
|
||||||
|
|
|
@ -21,6 +21,8 @@ use crate::npm::ManagedCliNpmResolver;
|
||||||
use crate::resolver::CliGraphResolver;
|
use crate::resolver::CliGraphResolver;
|
||||||
use crate::resolver::CliGraphResolverOptions;
|
use crate::resolver::CliGraphResolverOptions;
|
||||||
use crate::resolver::CliNodeResolver;
|
use crate::resolver::CliNodeResolver;
|
||||||
|
use crate::resolver::SloppyImportsFsEntry;
|
||||||
|
use crate::resolver::SloppyImportsResolver;
|
||||||
use crate::util::progress_bar::ProgressBar;
|
use crate::util::progress_bar::ProgressBar;
|
||||||
use crate::util::progress_bar::ProgressBarStyle;
|
use crate::util::progress_bar::ProgressBarStyle;
|
||||||
use deno_cache_dir::HttpCache;
|
use deno_cache_dir::HttpCache;
|
||||||
|
@ -60,6 +62,7 @@ pub struct LspResolver {
|
||||||
redirect_resolver: Option<Arc<RedirectResolver>>,
|
redirect_resolver: Option<Arc<RedirectResolver>>,
|
||||||
graph_imports: Arc<IndexMap<ModuleSpecifier, GraphImport>>,
|
graph_imports: Arc<IndexMap<ModuleSpecifier, GraphImport>>,
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
|
unstable_sloppy_imports: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LspResolver {
|
impl Default for LspResolver {
|
||||||
|
@ -73,6 +76,7 @@ impl Default for LspResolver {
|
||||||
redirect_resolver: None,
|
redirect_resolver: None,
|
||||||
graph_imports: Default::default(),
|
graph_imports: Default::default(),
|
||||||
config: Default::default(),
|
config: Default::default(),
|
||||||
|
unstable_sloppy_imports: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,6 +144,10 @@ impl LspResolver {
|
||||||
npm_config_hash,
|
npm_config_hash,
|
||||||
redirect_resolver,
|
redirect_resolver,
|
||||||
graph_imports,
|
graph_imports,
|
||||||
|
unstable_sloppy_imports: config_data
|
||||||
|
.and_then(|d| d.config_file.as_ref())
|
||||||
|
.map(|cf| cf.has_unstable("sloppy-imports"))
|
||||||
|
.unwrap_or(false),
|
||||||
config: Arc::new(config.clone()),
|
config: Arc::new(config.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -162,6 +170,7 @@ impl LspResolver {
|
||||||
redirect_resolver: self.redirect_resolver.clone(),
|
redirect_resolver: self.redirect_resolver.clone(),
|
||||||
graph_imports: self.graph_imports.clone(),
|
graph_imports: self.graph_imports.clone(),
|
||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
|
unstable_sloppy_imports: self.unstable_sloppy_imports,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,8 +190,11 @@ impl LspResolver {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_graph_resolver(&self) -> &dyn Resolver {
|
pub fn as_graph_resolver(&self) -> LspGraphResolver {
|
||||||
self.graph_resolver.as_ref()
|
LspGraphResolver {
|
||||||
|
inner: &self.graph_resolver,
|
||||||
|
unstable_sloppy_imports: self.unstable_sloppy_imports,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_graph_npm_resolver(&self) -> &dyn NpmResolver {
|
pub fn as_graph_npm_resolver(&self) -> &dyn NpmResolver {
|
||||||
|
@ -307,6 +319,68 @@ impl LspResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LspGraphResolver<'a> {
|
||||||
|
inner: &'a CliGraphResolver,
|
||||||
|
unstable_sloppy_imports: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Resolver for LspGraphResolver<'a> {
|
||||||
|
fn default_jsx_import_source(&self) -> Option<String> {
|
||||||
|
self.inner.default_jsx_import_source()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_jsx_import_source_types(&self) -> Option<String> {
|
||||||
|
self.inner.default_jsx_import_source_types()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jsx_import_source_module(&self) -> &str {
|
||||||
|
self.inner.jsx_import_source_module()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(
|
||||||
|
&self,
|
||||||
|
specifier_text: &str,
|
||||||
|
referrer_range: &deno_graph::Range,
|
||||||
|
mode: deno_graph::source::ResolutionMode,
|
||||||
|
) -> Result<deno_ast::ModuleSpecifier, deno_graph::source::ResolveError> {
|
||||||
|
let specifier = self.inner.resolve(specifier_text, referrer_range, mode)?;
|
||||||
|
if self.unstable_sloppy_imports && specifier.scheme() == "file" {
|
||||||
|
Ok(
|
||||||
|
SloppyImportsResolver::resolve_with_stat_sync(
|
||||||
|
&specifier,
|
||||||
|
mode,
|
||||||
|
|path| {
|
||||||
|
path.metadata().ok().and_then(|m| {
|
||||||
|
if m.is_file() {
|
||||||
|
Some(SloppyImportsFsEntry::File)
|
||||||
|
} else if m.is_dir() {
|
||||||
|
Some(SloppyImportsFsEntry::Dir)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_specifier()
|
||||||
|
.into_owned(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Ok(specifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_types(
|
||||||
|
&self,
|
||||||
|
specifier: &deno_ast::ModuleSpecifier,
|
||||||
|
) -> Result<
|
||||||
|
Option<(deno_ast::ModuleSpecifier, Option<deno_graph::Range>)>,
|
||||||
|
deno_graph::source::ResolveError,
|
||||||
|
> {
|
||||||
|
self.inner.resolve_types(specifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_npm_resolver(
|
async fn create_npm_resolver(
|
||||||
config_data: &ConfigData,
|
config_data: &ConfigData,
|
||||||
global_cache_path: Option<&Path>,
|
global_cache_path: Option<&Path>,
|
||||||
|
@ -399,9 +473,7 @@ fn create_graph_resolver(
|
||||||
bare_node_builtins_enabled: config_file
|
bare_node_builtins_enabled: config_file
|
||||||
.map(|cf| cf.has_unstable("bare-node-builtins"))
|
.map(|cf| cf.has_unstable("bare-node-builtins"))
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
// Don't set this for the LSP because instead we'll use the OpenDocumentsLoader
|
// not used in the LSP as the LspGraphResolver handles this
|
||||||
// because it's much easier and we get diagnostics/quick fixes about a redirected
|
|
||||||
// specifier for free.
|
|
||||||
sloppy_imports_resolver: None,
|
sloppy_imports_resolver: None,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,70 +12,13 @@ use test_util::assert_starts_with;
|
||||||
use test_util::assertions::assert_json_subset;
|
use test_util::assertions::assert_json_subset;
|
||||||
use test_util::deno_cmd_with_deno_dir;
|
use test_util::deno_cmd_with_deno_dir;
|
||||||
use test_util::env_vars_for_npm_tests;
|
use test_util::env_vars_for_npm_tests;
|
||||||
|
use test_util::lsp::range_of;
|
||||||
|
use test_util::lsp::source_file;
|
||||||
use test_util::lsp::LspClient;
|
use test_util::lsp::LspClient;
|
||||||
use test_util::testdata_path;
|
use test_util::testdata_path;
|
||||||
use test_util::TestContextBuilder;
|
use test_util::TestContextBuilder;
|
||||||
use tower_lsp::lsp_types as lsp;
|
use tower_lsp::lsp_types as lsp;
|
||||||
|
|
||||||
/// Helper to get the `lsp::Range` of the `n`th occurrence of
|
|
||||||
/// `text` in `src`. `n` is zero-based, like most indexes.
|
|
||||||
fn range_of_nth(
|
|
||||||
n: usize,
|
|
||||||
text: impl AsRef<str>,
|
|
||||||
src: impl AsRef<str>,
|
|
||||||
) -> lsp::Range {
|
|
||||||
let text = text.as_ref();
|
|
||||||
|
|
||||||
let src = src.as_ref();
|
|
||||||
|
|
||||||
let start = src
|
|
||||||
.match_indices(text)
|
|
||||||
.nth(n)
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
.unwrap_or_else(|| panic!("couldn't find text {text} in source {src}"));
|
|
||||||
let end = start + text.len();
|
|
||||||
let mut line = 0;
|
|
||||||
let mut col = 0;
|
|
||||||
let mut byte_idx = 0;
|
|
||||||
|
|
||||||
let pos = |line, col| lsp::Position {
|
|
||||||
line,
|
|
||||||
character: col,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut start_pos = None;
|
|
||||||
let mut end_pos = None;
|
|
||||||
for c in src.chars() {
|
|
||||||
if byte_idx == start {
|
|
||||||
start_pos = Some(pos(line, col));
|
|
||||||
}
|
|
||||||
if byte_idx == end {
|
|
||||||
end_pos = Some(pos(line, col));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if c == '\n' {
|
|
||||||
line += 1;
|
|
||||||
col = 0;
|
|
||||||
} else {
|
|
||||||
col += c.len_utf16() as u32;
|
|
||||||
}
|
|
||||||
byte_idx += c.len_utf8();
|
|
||||||
}
|
|
||||||
if start_pos.is_some() && end_pos.is_none() {
|
|
||||||
// range extends to end of string
|
|
||||||
end_pos = Some(pos(line, col));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (start, end) = (start_pos.unwrap(), end_pos.unwrap());
|
|
||||||
lsp::Range { start, end }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to get the `lsp::Range` of the first occurrence of
|
|
||||||
/// `text` in `src`. Equivalent to `range_of_nth(0, text, src)`.
|
|
||||||
fn range_of(text: impl AsRef<str>, src: impl AsRef<str>) -> lsp::Range {
|
|
||||||
range_of_nth(0, text, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lsp_startup_shutdown() {
|
fn lsp_startup_shutdown() {
|
||||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||||
|
@ -12106,15 +12049,19 @@ fn lsp_deno_future_env_byonm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lsp_sloppy_imports_warn() {
|
fn lsp_sloppy_imports() {
|
||||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||||
let temp_dir = context.temp_dir();
|
let temp_dir = context.temp_dir();
|
||||||
let temp_dir = temp_dir.path();
|
let temp_dir = temp_dir.path();
|
||||||
temp_dir
|
temp_dir
|
||||||
.join("deno.json")
|
.join("deno.json")
|
||||||
.write(r#"{ "unstable": ["sloppy-imports"] }"#);
|
.write(r#"{ "unstable": ["sloppy-imports"] }"#);
|
||||||
// should work when exists on the fs and when doesn't
|
// for sloppy imports, the file must exist on the file system
|
||||||
|
// to be resolved correctly
|
||||||
temp_dir.join("a.ts").write("export class A {}");
|
temp_dir.join("a.ts").write("export class A {}");
|
||||||
|
temp_dir.join("b.ts").write("export class B {}");
|
||||||
|
temp_dir.join("c.js").write("export class C {}");
|
||||||
|
temp_dir.join("c.d.ts").write("export class C {}");
|
||||||
let mut client = context.new_lsp_command().build();
|
let mut client = context.new_lsp_command().build();
|
||||||
client.initialize(|builder| {
|
client.initialize(|builder| {
|
||||||
builder.set_root_uri(temp_dir.uri_dir());
|
builder.set_root_uri(temp_dir.uri_dir());
|
||||||
|
@ -12161,137 +12108,67 @@ fn lsp_sloppy_imports_warn() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
assert_eq!(
|
|
||||||
diagnostics.messages_with_source("deno"),
|
|
||||||
lsp::PublishDiagnosticsParams {
|
|
||||||
uri: temp_dir.join("file.ts").uri_file(),
|
|
||||||
diagnostics: vec![
|
|
||||||
lsp::Diagnostic {
|
|
||||||
range: lsp::Range {
|
|
||||||
start: lsp::Position {
|
|
||||||
line: 0,
|
|
||||||
character: 19
|
|
||||||
},
|
|
||||||
end: lsp::Position {
|
|
||||||
line: 0,
|
|
||||||
character: 24
|
|
||||||
}
|
|
||||||
},
|
|
||||||
severity: Some(lsp::DiagnosticSeverity::INFORMATION),
|
|
||||||
code: Some(lsp::NumberOrString::String("redirect".to_string())),
|
|
||||||
source: Some("deno".to_string()),
|
|
||||||
message: format!(
|
|
||||||
"The import of \"{}\" was redirected to \"{}\".",
|
|
||||||
temp_dir.join("a").uri_file(),
|
|
||||||
temp_dir.join("a.ts").uri_file()
|
|
||||||
),
|
|
||||||
data: Some(json!({
|
|
||||||
"specifier": temp_dir.join("a").uri_file(),
|
|
||||||
"redirect": temp_dir.join("a.ts").uri_file()
|
|
||||||
})),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
lsp::Diagnostic {
|
|
||||||
range: lsp::Range {
|
|
||||||
start: lsp::Position {
|
|
||||||
line: 1,
|
|
||||||
character: 19
|
|
||||||
},
|
|
||||||
end: lsp::Position {
|
|
||||||
line: 1,
|
|
||||||
character: 27
|
|
||||||
}
|
|
||||||
},
|
|
||||||
severity: Some(lsp::DiagnosticSeverity::INFORMATION),
|
|
||||||
code: Some(lsp::NumberOrString::String("redirect".to_string())),
|
|
||||||
source: Some("deno".to_string()),
|
|
||||||
message: format!(
|
|
||||||
"The import of \"{}\" was redirected to \"{}\".",
|
|
||||||
temp_dir.join("b.js").uri_file(),
|
|
||||||
temp_dir.join("b.ts").uri_file()
|
|
||||||
),
|
|
||||||
data: Some(json!({
|
|
||||||
"specifier": temp_dir.join("b.js").uri_file(),
|
|
||||||
"redirect": temp_dir.join("b.ts").uri_file()
|
|
||||||
})),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
version: Some(1),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let res = client.write_request(
|
assert_eq!(json!(diagnostics.all()), json!([]));
|
||||||
"textDocument/codeAction",
|
|
||||||
|
client.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lsp_sloppy_imports_prefers_dts() {
|
||||||
|
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||||
|
let temp_dir = context.temp_dir();
|
||||||
|
let temp_dir = temp_dir.path();
|
||||||
|
|
||||||
|
temp_dir
|
||||||
|
.join("deno.json")
|
||||||
|
.write(r#"{ "unstable": ["sloppy-imports"] }"#);
|
||||||
|
|
||||||
|
let mut client: LspClient = context
|
||||||
|
.new_lsp_command()
|
||||||
|
.set_root_dir(temp_dir.clone())
|
||||||
|
.build();
|
||||||
|
client.initialize_default();
|
||||||
|
|
||||||
|
temp_dir.join("a.js").write("export const foo: number;");
|
||||||
|
|
||||||
|
let a_dts = source_file(temp_dir.join("a.d.ts"), "export const foo = 3;");
|
||||||
|
let file = source_file(
|
||||||
|
temp_dir.join("file.ts"),
|
||||||
|
"import { foo } from './a.js';\nconsole.log(foo);",
|
||||||
|
);
|
||||||
|
let diagnostics = client.did_open_file(&file);
|
||||||
|
// no warnings because "a.js" exists
|
||||||
|
assert_eq!(diagnostics.all().len(), 0);
|
||||||
|
|
||||||
|
let diagnostics = client.did_open_file(&a_dts);
|
||||||
|
assert_eq!(diagnostics.all().len(), 0, "Got {:#?}", diagnostics.all());
|
||||||
|
|
||||||
|
let response = client.write_request(
|
||||||
|
"textDocument/references",
|
||||||
json!({
|
json!({
|
||||||
"textDocument": {
|
"textDocument": a_dts.identifier(),
|
||||||
"uri": temp_dir.join("file.ts").uri_file()
|
"position": a_dts.range_of("foo").start,
|
||||||
},
|
|
||||||
"range": {
|
|
||||||
"start": { "line": 0, "character": 19 },
|
|
||||||
"end": { "line": 0, "character": 24 }
|
|
||||||
},
|
|
||||||
"context": {
|
"context": {
|
||||||
"diagnostics": [{
|
"includeDeclaration": false
|
||||||
"range": {
|
|
||||||
"start": { "line": 0, "character": 19 },
|
|
||||||
"end": { "line": 0, "character": 24 }
|
|
||||||
},
|
|
||||||
"severity": 3,
|
|
||||||
"code": "redirect",
|
|
||||||
"source": "deno",
|
|
||||||
"message": format!(
|
|
||||||
"The import of \"{}\" was redirected to \"{}\".",
|
|
||||||
temp_dir.join("a").uri_file(),
|
|
||||||
temp_dir.join("a.ts").uri_file()
|
|
||||||
),
|
|
||||||
"data": {
|
|
||||||
"specifier": temp_dir.join("a").uri_file(),
|
|
||||||
"redirect": temp_dir.join("a.ts").uri_file(),
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
"only": ["quickfix"]
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_json_subset(
|
||||||
res,
|
response,
|
||||||
json!([{
|
json!([
|
||||||
"title": "Update specifier to its redirected specifier.",
|
{
|
||||||
"kind": "quickfix",
|
"uri": file.uri(),
|
||||||
"diagnostics": [{
|
// the import
|
||||||
"range": {
|
"range": file.range_of("foo"),
|
||||||
"start": { "line": 0, "character": 19 },
|
},
|
||||||
"end": { "line": 0, "character": 24 }
|
{
|
||||||
},
|
"uri": file.uri(),
|
||||||
"severity": 3,
|
// the usage
|
||||||
"code": "redirect",
|
"range": file.range_of_nth(1, "foo"),
|
||||||
"source": "deno",
|
|
||||||
"message": format!(
|
|
||||||
"The import of \"{}\" was redirected to \"{}\".",
|
|
||||||
temp_dir.join("a").uri_file(),
|
|
||||||
temp_dir.join("a.ts").uri_file()
|
|
||||||
),
|
|
||||||
"data": {
|
|
||||||
"specifier": temp_dir.join("a").uri_file(),
|
|
||||||
"redirect": temp_dir.join("a.ts").uri_file()
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
"edit": {
|
|
||||||
"changes": {
|
|
||||||
temp_dir.join("file.ts").uri_file(): [{
|
|
||||||
"range": {
|
|
||||||
"start": { "line": 0, "character": 19 },
|
|
||||||
"end": { "line": 0, "character": 24 }
|
|
||||||
},
|
|
||||||
"newText": "\"./a.ts\""
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}])
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
client.shutdown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -778,6 +778,12 @@ impl LspClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn did_open_file(&mut self, file: &SourceFile) -> CollectedDiagnostics {
|
||||||
|
self.did_open(json!({
|
||||||
|
"textDocument": file.text_document(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn did_open(&mut self, params: Value) -> CollectedDiagnostics {
|
pub fn did_open(&mut self, params: Value) -> CollectedDiagnostics {
|
||||||
self.did_open_raw(params);
|
self.did_open_raw(params);
|
||||||
self.read_diagnostics()
|
self.read_diagnostics()
|
||||||
|
@ -1138,6 +1144,130 @@ impl CollectedDiagnostics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SourceFile {
|
||||||
|
path: PathRef,
|
||||||
|
src: String,
|
||||||
|
lang: &'static str,
|
||||||
|
version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceFile {
|
||||||
|
pub fn new(path: PathRef, src: String) -> Self {
|
||||||
|
path.write(&src);
|
||||||
|
Self::new_in_mem(path, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_in_mem(path: PathRef, src: String) -> Self {
|
||||||
|
let lang = match path.as_path().extension().unwrap().to_str().unwrap() {
|
||||||
|
"js" => "javascript",
|
||||||
|
"ts" | "d.ts" => "typescript",
|
||||||
|
"json" => "json",
|
||||||
|
other => panic!("unsupported file extension: {other}"),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
src,
|
||||||
|
lang,
|
||||||
|
version: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_of(&self, text: &str) -> lsp::Range {
|
||||||
|
range_of(text, &self.src)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_of_nth(&self, n: usize, text: &str) -> lsp::Range {
|
||||||
|
range_of_nth(n, text, &self.src)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uri(&self) -> lsp::Url {
|
||||||
|
self.path.uri_file()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_document(&self) -> lsp::TextDocumentItem {
|
||||||
|
lsp::TextDocumentItem {
|
||||||
|
uri: self.uri(),
|
||||||
|
language_id: self.lang.to_string(),
|
||||||
|
version: self.version,
|
||||||
|
text: self.src.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identifier(&self) -> lsp::TextDocumentIdentifier {
|
||||||
|
lsp::TextDocumentIdentifier { uri: self.uri() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to create a `SourceFile` and write its contents to disk.
|
||||||
|
pub fn source_file(path: PathRef, src: impl AsRef<str>) -> SourceFile {
|
||||||
|
SourceFile::new(path, src.as_ref().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to create a `SourceFile` in memory without writing to disk.
|
||||||
|
pub fn source_file_in_mem(path: PathRef, src: impl AsRef<str>) -> SourceFile {
|
||||||
|
SourceFile::new_in_mem(path, src.as_ref().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to get the `lsp::Range` of the `n`th occurrence of
|
||||||
|
/// `text` in `src`. `n` is zero-based, like most indexes.
|
||||||
|
pub fn range_of_nth(
|
||||||
|
n: usize,
|
||||||
|
text: impl AsRef<str>,
|
||||||
|
src: impl AsRef<str>,
|
||||||
|
) -> lsp::Range {
|
||||||
|
let text = text.as_ref();
|
||||||
|
|
||||||
|
let src = src.as_ref();
|
||||||
|
|
||||||
|
let start = src
|
||||||
|
.match_indices(text)
|
||||||
|
.nth(n)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or_else(|| panic!("couldn't find text {text} in source {src}"));
|
||||||
|
let end = start + text.len();
|
||||||
|
let mut line = 0;
|
||||||
|
let mut col = 0;
|
||||||
|
let mut byte_idx = 0;
|
||||||
|
|
||||||
|
let pos = |line, col| lsp::Position {
|
||||||
|
line,
|
||||||
|
character: col,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut start_pos = None;
|
||||||
|
let mut end_pos = None;
|
||||||
|
for c in src.chars() {
|
||||||
|
if byte_idx == start {
|
||||||
|
start_pos = Some(pos(line, col));
|
||||||
|
}
|
||||||
|
if byte_idx == end {
|
||||||
|
end_pos = Some(pos(line, col));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if c == '\n' {
|
||||||
|
line += 1;
|
||||||
|
col = 0;
|
||||||
|
} else {
|
||||||
|
col += c.len_utf16() as u32;
|
||||||
|
}
|
||||||
|
byte_idx += c.len_utf8();
|
||||||
|
}
|
||||||
|
if start_pos.is_some() && end_pos.is_none() {
|
||||||
|
// range extends to end of string
|
||||||
|
end_pos = Some(pos(line, col));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (start, end) = (start_pos.unwrap(), end_pos.unwrap());
|
||||||
|
lsp::Range { start, end }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to get the `lsp::Range` of the first occurrence of
|
||||||
|
/// `text` in `src`. Equivalent to `range_of_nth(0, text, src)`.
|
||||||
|
pub fn range_of(text: impl AsRef<str>, src: impl AsRef<str>) -> lsp::Range {
|
||||||
|
range_of_nth(0, text, src)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
Loading…
Reference in a new issue