diff options
Diffstat (limited to 'cli/lsp/sources.rs')
-rw-r--r-- | cli/lsp/sources.rs | 469 |
1 files changed, 271 insertions, 198 deletions
diff --git a/cli/lsp/sources.rs b/cli/lsp/sources.rs index 62316e4a2..307170d72 100644 --- a/cli/lsp/sources.rs +++ b/cli/lsp/sources.rs @@ -40,9 +40,68 @@ pub async fn cache( builder.add(specifier, false).await } +fn get_remote_headers( + cache_filename: &Path, +) -> Option<HashMap<String, String>> { + let metadata_path = http_cache::Metadata::filename(cache_filename); + let metadata_str = fs::read_to_string(metadata_path).ok()?; + let metadata: http_cache::Metadata = + serde_json::from_str(&metadata_str).ok()?; + Some(metadata.headers) +} + +fn resolve_remote_specifier( + specifier: &ModuleSpecifier, + http_cache: &HttpCache, + redirect_limit: isize, +) -> Option<ModuleSpecifier> { + let cache_filename = http_cache.get_cache_filename(specifier.as_url())?; + if redirect_limit >= 0 && cache_filename.is_file() { + let headers = get_remote_headers(&cache_filename)?; + if let Some(location) = headers.get("location") { + let redirect = + ModuleSpecifier::resolve_import(location, specifier.as_str()).ok()?; + resolve_remote_specifier(&redirect, http_cache, redirect_limit - 1) + } else { + Some(specifier.clone()) + } + } else { + None + } +} + +fn resolve_specifier( + specifier: &ModuleSpecifier, + redirects: &mut HashMap<ModuleSpecifier, ModuleSpecifier>, + http_cache: &HttpCache, +) -> Option<ModuleSpecifier> { + let scheme = specifier.as_url().scheme(); + if !SUPPORTED_SCHEMES.contains(&scheme) { + return None; + } + + if scheme == "data" { + Some(specifier.clone()) + } else if scheme == "file" { + let path = specifier.as_url().to_file_path().ok()?; + if path.is_file() { + Some(specifier.clone()) + } else { + None + } + } else if let Some(specifier) = redirects.get(specifier) { + Some(specifier.clone()) + } else { + let redirect = resolve_remote_specifier(specifier, http_cache, 10)?; + redirects.insert(specifier.clone(), redirect.clone()); + Some(redirect) + } +} + #[derive(Debug, Clone, Default)] struct Metadata { dependencies: Option<HashMap<String, analysis::Dependency>>, + length_utf16: usize, line_index: LineIndex, maybe_types: Option<analysis::ResolvedDependency>, media_type: MediaType, @@ -50,6 +109,39 @@ struct Metadata { version: String, } +impl Metadata { + fn new( + specifier: &ModuleSpecifier, + source: &str, + version: &str, + media_type: &MediaType, + maybe_import_map: &Option<ImportMap>, + ) -> Self { + let (dependencies, maybe_types) = if let Some((dependencies, maybe_types)) = + analysis::analyze_dependencies( + specifier, + source, + media_type, + maybe_import_map, + ) { + (Some(dependencies), maybe_types) + } else { + (None, None) + }; + let line_index = LineIndex::new(source); + + Self { + dependencies, + length_utf16: source.encode_utf16().count(), + line_index, + maybe_types, + media_type: media_type.to_owned(), + source: source.to_string(), + version: version.to_string(), + } + } +} + #[derive(Debug, Clone, Default)] struct Inner { http_cache: HttpCache, @@ -71,6 +163,10 @@ impl Sources { self.0.lock().unwrap().contains_key(specifier) } + /// Provides the length of the source content, calculated in a way that should + /// match the behavior of JavaScript, where strings are stored effectively as + /// `&[u16]` and when counting "chars" we need to represent the string as a + /// UTF-16 string in Rust. pub fn get_length_utf16(&self, specifier: &ModuleSpecifier) -> Option<usize> { self.0.lock().unwrap().get_length_utf16(specifier) } @@ -103,8 +199,8 @@ impl Sources { self.0.lock().unwrap().get_script_version(specifier) } - pub fn get_text(&self, specifier: &ModuleSpecifier) -> Option<String> { - self.0.lock().unwrap().get_text(specifier) + pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option<String> { + self.0.lock().unwrap().get_source(specifier) } pub fn resolve_import( @@ -114,14 +210,6 @@ impl Sources { ) -> Option<(ModuleSpecifier, MediaType)> { self.0.lock().unwrap().resolve_import(specifier, referrer) } - - #[cfg(test)] - fn resolve_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Option<ModuleSpecifier> { - self.0.lock().unwrap().resolve_specifier(specifier) - } } impl Inner { @@ -132,8 +220,27 @@ impl Inner { } } + fn calculate_script_version( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option<String> { + let path = self.get_path(specifier)?; + let metadata = fs::metadata(path).ok()?; + if let Ok(modified) = metadata.modified() { + if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) { + Some(format!("{}", n.as_millis())) + } else { + Some("1".to_string()) + } + } else { + Some("1".to_string()) + } + } + fn contains_key(&mut self, specifier: &ModuleSpecifier) -> bool { - if let Some(specifier) = self.resolve_specifier(specifier) { + if let Some(specifier) = + resolve_specifier(specifier, &mut self.redirects, &self.http_cache) + { if self.get_metadata(&specifier).is_some() { return true; } @@ -141,21 +248,19 @@ impl Inner { false } - /// Provides the length of the source content, calculated in a way that should - /// match the behavior of JavaScript, where strings are stored effectively as - /// `&[u16]` and when counting "chars" we need to represent the string as a - /// UTF-16 string in Rust. fn get_length_utf16(&mut self, specifier: &ModuleSpecifier) -> Option<usize> { - let specifier = self.resolve_specifier(specifier)?; + let specifier = + resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; let metadata = self.get_metadata(&specifier)?; - Some(metadata.source.encode_utf16().count()) + Some(metadata.length_utf16) } fn get_line_index( &mut self, specifier: &ModuleSpecifier, ) -> Option<LineIndex> { - let specifier = self.resolve_specifier(specifier)?; + let specifier = + resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; let metadata = self.get_metadata(&specifier)?; Some(metadata.line_index) } @@ -164,7 +269,9 @@ impl Inner { &mut self, specifier: &ModuleSpecifier, ) -> Option<analysis::ResolvedDependency> { - let metadata = self.get_metadata(specifier)?; + let specifier = + resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; + let metadata = self.get_metadata(&specifier)?; metadata.maybe_types } @@ -172,118 +279,62 @@ impl Inner { &mut self, specifier: &ModuleSpecifier, ) -> Option<MediaType> { - let specifier = self.resolve_specifier(specifier)?; + let specifier = + resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; let metadata = self.get_metadata(&specifier)?; Some(metadata.media_type) } fn get_metadata(&mut self, specifier: &ModuleSpecifier) -> Option<Metadata> { if let Some(metadata) = self.metadata.get(specifier).cloned() { - if metadata.version == self.get_script_version(specifier)? { + if metadata.version == self.calculate_script_version(specifier)? { return Some(metadata); } } - // TODO(@kitsonk) this needs to be refactored, lots of duplicate logic and - // is really difficult to follow. - let version = self.get_script_version(specifier)?; + let version = self.calculate_script_version(specifier)?; let path = self.get_path(specifier)?; - if let Ok(bytes) = fs::read(path) { - if specifier.as_url().scheme() == "file" { - let charset = text_encoding::detect_charset(&bytes).to_string(); - if let Ok(source) = get_source_from_bytes(bytes, Some(charset)) { - let media_type = MediaType::from(specifier); - let mut maybe_types = None; - let maybe_import_map = self.maybe_import_map.clone(); - let dependencies = if let Some((dependencies, mt)) = - analysis::analyze_dependencies( - &specifier, - &source, - &media_type, - &maybe_import_map, - ) { - maybe_types = mt; - Some(dependencies) - } else { - None - }; - let line_index = LineIndex::new(&source); - let metadata = Metadata { - dependencies, - line_index, - maybe_types, - media_type, - source, - version, - }; - self.metadata.insert(specifier.clone(), metadata.clone()); - Some(metadata) - } else { - None - } - } else { - let headers = self.get_remote_headers(specifier)?; - let maybe_content_type = headers.get("content-type").cloned(); - let (media_type, maybe_charset) = - map_content_type(specifier, maybe_content_type); - if let Ok(source) = get_source_from_bytes(bytes, maybe_charset) { - let mut maybe_types = - if let Some(types) = headers.get("x-typescript-types") { - Some(analysis::resolve_import( - types, - &specifier, - &self.maybe_import_map, - )) - } else { - None - }; - let maybe_import_map = self.maybe_import_map.clone(); - let dependencies = if let Some((dependencies, mt)) = - analysis::analyze_dependencies( - &specifier, - &source, - &media_type, - &maybe_import_map, - ) { - if maybe_types.is_none() { - maybe_types = mt; - } - Some(dependencies) - } else { - None - }; - let line_index = LineIndex::new(&source); - let metadata = Metadata { - dependencies, - line_index, - maybe_types, - media_type, - source, - version, - }; - self.metadata.insert(specifier.clone(), metadata.clone()); - Some(metadata) - } else { - None - } - } + let bytes = fs::read(path).ok()?; + let scheme = specifier.as_url().scheme(); + let (source, media_type, maybe_types) = if scheme == "file" { + let maybe_charset = + Some(text_encoding::detect_charset(&bytes).to_string()); + let source = get_source_from_bytes(bytes, maybe_charset).ok()?; + (source, MediaType::from(specifier), None) } else { - None + let cache_filename = + self.http_cache.get_cache_filename(specifier.as_url())?; + let headers = get_remote_headers(&cache_filename)?; + let maybe_content_type = headers.get("content-type").cloned(); + let (media_type, maybe_charset) = + map_content_type(specifier, maybe_content_type); + let source = get_source_from_bytes(bytes, maybe_charset).ok()?; + let maybe_types = headers.get("x-typescript-types").map(|s| { + analysis::resolve_import(s, &specifier, &self.maybe_import_map) + }); + (source, media_type, maybe_types) + }; + let mut metadata = Metadata::new( + specifier, + &source, + &version, + &media_type, + &self.maybe_import_map, + ); + if metadata.maybe_types.is_none() { + metadata.maybe_types = maybe_types; } + self.metadata.insert(specifier.clone(), metadata.clone()); + Some(metadata) } fn get_path(&mut self, specifier: &ModuleSpecifier) -> Option<PathBuf> { - let specifier = self.resolve_specifier(specifier)?; if specifier.as_url().scheme() == "file" { - if let Ok(path) = specifier.as_url().to_file_path() { - Some(path) - } else { - None - } + specifier.as_url().to_file_path().ok() } else if let Some(path) = self.remotes.get(&specifier) { Some(path.clone()) } else { - let path = self.http_cache.get_cache_filename(&specifier.as_url()); + let path = self.http_cache.get_cache_filename(&specifier.as_url())?; if path.is_file() { self.remotes.insert(specifier.clone(), path.clone()); Some(path) @@ -293,57 +344,35 @@ impl Inner { } } - fn get_remote_headers( - &self, - specifier: &ModuleSpecifier, - ) -> Option<HashMap<String, String>> { - let cache_filename = self.http_cache.get_cache_filename(specifier.as_url()); - let metadata_path = http_cache::Metadata::filename(&cache_filename); - if let Ok(metadata) = fs::read_to_string(metadata_path) { - if let Ok(metadata) = - serde_json::from_str::<'_, http_cache::Metadata>(&metadata) - { - return Some(metadata.headers); - } - } - None - } - fn get_script_version( &mut self, specifier: &ModuleSpecifier, ) -> Option<String> { - let path = self.get_path(specifier)?; - let metadata = fs::metadata(path).ok()?; - if let Ok(modified) = metadata.modified() { - if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) { - Some(format!("{}", n.as_millis())) - } else { - Some("1".to_string()) - } - } else { - Some("1".to_string()) - } + let specifier = + resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; + let metadata = self.get_metadata(&specifier)?; + Some(metadata.version) } - fn get_text(&mut self, specifier: &ModuleSpecifier) -> Option<String> { - let specifier = self.resolve_specifier(specifier)?; + fn get_source(&mut self, specifier: &ModuleSpecifier) -> Option<String> { + let specifier = + resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; let metadata = self.get_metadata(&specifier)?; Some(metadata.source) } fn resolution_result( &mut self, - resolved_specifier: &ModuleSpecifier, + specifier: &ModuleSpecifier, ) -> Option<(ModuleSpecifier, MediaType)> { - let resolved_specifier = self.resolve_specifier(resolved_specifier)?; - let media_type = - if let Some(metadata) = self.metadata.get(&resolved_specifier) { - metadata.media_type - } else { - MediaType::from(&resolved_specifier) - }; - Some((resolved_specifier, media_type)) + let specifier = + resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?; + let media_type = if let Some(metadata) = self.metadata.get(&specifier) { + metadata.media_type + } else { + MediaType::from(&specifier) + }; + Some((specifier, media_type)) } fn resolve_import( @@ -351,7 +380,8 @@ impl Inner { specifier: &str, referrer: &ModuleSpecifier, ) -> Option<(ModuleSpecifier, MediaType)> { - let referrer = self.resolve_specifier(referrer)?; + let referrer = + resolve_specifier(referrer, &mut self.redirects, &self.http_cache)?; let metadata = self.get_metadata(&referrer)?; let dependencies = &metadata.dependencies?; let dependency = dependencies.get(specifier)?; @@ -368,62 +398,38 @@ impl Inner { if let analysis::ResolvedDependency::Resolved(resolved_specifier) = code_dependency { - self.resolution_result(resolved_specifier) + if let Some(type_dependency) = self.get_maybe_types(resolved_specifier) + { + self.set_maybe_type(specifier, &referrer, &type_dependency); + if let analysis::ResolvedDependency::Resolved(type_specifier) = + type_dependency + { + self.resolution_result(&type_specifier) + } else { + self.resolution_result(resolved_specifier) + } + } else { + self.resolution_result(resolved_specifier) + } } else { None } } } - fn resolve_specifier( + fn set_maybe_type( &mut self, - specifier: &ModuleSpecifier, - ) -> Option<ModuleSpecifier> { - let scheme = specifier.as_url().scheme(); - if !SUPPORTED_SCHEMES.contains(&scheme) { - return None; - } - - if scheme == "file" { - if let Ok(path) = specifier.as_url().to_file_path() { - if path.is_file() { - return Some(specifier.clone()); - } - } - } else { - if let Some(specifier) = self.redirects.get(specifier) { - return Some(specifier.clone()); - } - if let Some(redirect) = self.resolve_remote_specifier(specifier, 10) { - self.redirects.insert(specifier.clone(), redirect.clone()); - return Some(redirect); - } - } - None - } - - fn resolve_remote_specifier( - &self, - specifier: &ModuleSpecifier, - redirect_limit: isize, - ) -> Option<ModuleSpecifier> { - let cached_filename = - self.http_cache.get_cache_filename(specifier.as_url()); - if redirect_limit >= 0 && cached_filename.is_file() { - if let Some(headers) = self.get_remote_headers(specifier) { - if let Some(redirect_to) = headers.get("location") { - if let Ok(redirect) = - ModuleSpecifier::resolve_import(redirect_to, specifier.as_str()) - { - return self - .resolve_remote_specifier(&redirect, redirect_limit - 1); - } - } else { - return Some(specifier.clone()); + specifier: &str, + referrer: &ModuleSpecifier, + dependency: &analysis::ResolvedDependency, + ) { + if let Some(metadata) = self.metadata.get_mut(referrer) { + if let Some(dependencies) = &mut metadata.dependencies { + if let Some(dep) = dependencies.get_mut(specifier) { + dep.maybe_type = Some(dependency.clone()); } } } - None } } @@ -462,7 +468,7 @@ mod tests { &tests.join("001_hello.js").to_string_lossy(), ) .unwrap(); - let actual = sources.get_text(&specifier); + let actual = sources.get_source(&specifier); assert!(actual.is_some()); let actual = actual.unwrap(); assert_eq!(actual, "console.log(\"Hello World\");\n"); @@ -484,11 +490,78 @@ mod tests { } #[test] + fn test_resolve_dependency_types() { + let (sources, location) = setup(); + let cache = HttpCache::new(&location); + let specifier_dep = + ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap(); + cache + .set( + specifier_dep.as_url(), + Default::default(), + b"export * from \"https://deno.land/x/lib.js\";", + ) + .unwrap(); + let specifier_code = + ModuleSpecifier::resolve_url("https://deno.land/x/lib.js").unwrap(); + let mut headers_code = HashMap::new(); + headers_code + .insert("x-typescript-types".to_string(), "./lib.d.ts".to_string()); + cache + .set( + specifier_code.as_url(), + headers_code, + b"export const a = 1;", + ) + .unwrap(); + let specifier_type = + ModuleSpecifier::resolve_url("https://deno.land/x/lib.d.ts").unwrap(); + cache + .set( + specifier_type.as_url(), + Default::default(), + b"export const a: number;", + ) + .unwrap(); + let actual = + sources.resolve_import("https://deno.land/x/lib.js", &specifier_dep); + assert_eq!(actual, Some((specifier_type, MediaType::Dts))) + } + + #[test] + fn test_resolve_dependency_evil_redirect() { + let (sources, location) = setup(); + let cache = HttpCache::new(&location); + let evil_specifier = + ModuleSpecifier::resolve_url("https://deno.land/x/evil.ts").unwrap(); + let mut evil_headers = HashMap::new(); + evil_headers + .insert("location".to_string(), "file:///etc/passwd".to_string()); + cache + .set(evil_specifier.as_url(), evil_headers, b"") + .unwrap(); + let remote_specifier = + ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap(); + cache + .set( + remote_specifier.as_url(), + Default::default(), + b"export * from \"./evil.ts\";", + ) + .unwrap(); + let actual = sources.resolve_import("./evil.ts", &remote_specifier); + assert_eq!(actual, None); + } + + #[test] fn test_sources_resolve_specifier_non_supported_schema() { let (sources, _) = setup(); let specifier = ModuleSpecifier::resolve_url("foo://a/b/c.ts") .expect("could not create specifier"); - let actual = sources.resolve_specifier(&specifier); + let sources = sources.0.lock().unwrap(); + let mut redirects = sources.redirects.clone(); + let http_cache = sources.http_cache.clone(); + let actual = resolve_specifier(&specifier, &mut redirects, &http_cache); assert!(actual.is_none()); } } |