use std::any::Any;
use std::pin::Pin;
use std::sync::Arc;
#[cfg(feature = "lsp-responses")]
use tower_lsp_server::ls_types::{
CodeAction, CodeLens, DocumentLink, InlayHint, Position, TextEdit,
};
#[cfg(feature = "lsp-responses")]
use crate::completion::Completions;
#[cfg(feature = "lsp-responses")]
use crate::hover::Hover;
use crate::{
Registry,
diagnostic::Diagnostic,
lsp_helpers::{EcosystemFormatter, VersionData},
position::Range,
registry::Metadata,
selection::{InvalidStabilityOccurrence, SelectionContext},
};
#[doc(hidden)]
pub mod private {
pub trait Sealed {}
}
pub type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
pub async fn parse_manifest_blocking(
ecosystem: &Arc<dyn Ecosystem>,
content: &str,
uri: &url::Url,
) -> crate::error::Result<Box<dyn ParseResult>> {
let ecosystem = Arc::clone(ecosystem);
let owned_content = content.to_owned();
let owned_uri = uri.clone();
let handle = tokio::runtime::Handle::current();
let parsed = tokio::task::spawn_blocking(move || {
handle.block_on(ecosystem.parse_manifest(&owned_content, &owned_uri))
})
.await
.map_err(|e| crate::error::DepsError::parse_error(format!("manifest at {uri:?}"), &e))??;
Ok(crate::dependency_cap::cap_dependencies(
parsed,
crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT,
))
}
pub fn parse_manifest_now(
ecosystem: &dyn Ecosystem,
content: &str,
uri: &url::Url,
) -> Option<Box<dyn ParseResult>> {
tokio::runtime::Handle::try_current().ok()?;
let parsed = ecosystem.parse_manifest_sync(content, uri)?;
Some(crate::dependency_cap::cap_dependencies(
parsed,
crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT,
))
}
macro_rules! ecosystem_ids {
( $( $(#[$vmeta:meta])* $variant:ident => $id:literal ),+ $(,)? ) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EcosystemId {
$( $(#[$vmeta])* $variant, )+
}
impl EcosystemId {
pub const ALL: &'static [Self] = &[ $( Self::$variant ),+ ];
#[must_use]
pub const fn id(self) -> &'static str {
match self {
$( Self::$variant => $id, )+
}
}
}
impl std::str::FromStr for EcosystemId {
type Err = crate::error::DepsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$( $id => Ok(Self::$variant), )+
_ => Err(crate::error::DepsError::UnsupportedEcosystem(s.to_string())),
}
}
}
};
}
ecosystem_ids! {
Cargo => "cargo",
Npm => "npm",
Pypi => "pypi",
Go => "go",
Bundler => "bundler",
Dart => "dart",
Maven => "maven",
Composer => "composer",
Gradle => "gradle",
Swift => "swift",
NuGet => "nuget",
Deno => "deno",
GithubActions => "github-actions",
GitlabCi => "gitlab-ci",
}
impl EcosystemId {
#[must_use]
pub const fn osv_ecosystem(self) -> Option<crate::osv::OsvEcosystem> {
use crate::osv::OsvEcosystem;
match self {
Self::Cargo => Some(OsvEcosystem::CratesIo),
Self::Npm | Self::Deno => Some(OsvEcosystem::Npm),
Self::Pypi => Some(OsvEcosystem::PyPI),
Self::Go => Some(OsvEcosystem::Go),
Self::Bundler => Some(OsvEcosystem::RubyGems),
Self::Dart => Some(OsvEcosystem::Pub),
Self::Maven | Self::Gradle => Some(OsvEcosystem::Maven),
Self::Composer => Some(OsvEcosystem::Packagist),
Self::Swift => Some(OsvEcosystem::SwiftURL),
Self::NuGet => Some(OsvEcosystem::NuGet),
Self::GithubActions => Some(OsvEcosystem::GitHubActions),
Self::GitlabCi => None,
}
}
}
impl std::fmt::Display for EcosystemId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.id())
}
}
pub trait RegistryOccurrence {
fn range(&self) -> Range;
fn declaration_key(&self) -> &str;
}
#[derive(Clone, crate::redact_debug::RedactingDebug)]
pub struct BlockedRegistryOccurrence {
#[raw]
pub range: Range,
#[raw]
pub class: crate::net_policy::HostClass,
#[redact(url)]
pub raw_value: String,
#[redact(key)]
pub declaration_key: String,
}
impl RegistryOccurrence for BlockedRegistryOccurrence {
fn range(&self) -> Range {
self.range
}
fn declaration_key(&self) -> &str {
&self.declaration_key
}
}
#[derive(Clone, PartialEq, Eq, crate::redact_debug::RedactingDebug)]
pub struct BlockedSourceClass {
#[raw]
pub class: crate::net_policy::HostClass,
#[redact(url)]
pub raw_value: String,
#[redact(key)]
pub declaration_key: String,
}
impl BlockedSourceClass {
#[must_use]
pub fn into_occurrence(self, range: Range) -> BlockedRegistryOccurrence {
BlockedRegistryOccurrence {
range,
class: self.class,
raw_value: self.raw_value,
declaration_key: self.declaration_key,
}
}
}
#[derive(Clone, crate::redact_debug::RedactingDebug)]
pub struct RejectedRegistryOccurrence {
#[raw]
pub range: Range,
#[raw]
pub reason: crate::net_policy::RegistryRejectionReason,
#[redact(url)]
pub raw_value: String,
#[redact(key)]
pub declaration_key: String,
}
impl RegistryOccurrence for RejectedRegistryOccurrence {
fn range(&self) -> Range {
self.range
}
fn declaration_key(&self) -> &str {
&self.declaration_key
}
}
#[derive(Clone, PartialEq, Eq, crate::redact_debug::RedactingDebug)]
pub struct RejectedSourceClass {
#[raw]
pub reason: crate::net_policy::RegistryRejectionReason,
#[redact(url)]
pub raw_value: String,
#[redact(key)]
pub declaration_key: String,
}
impl RejectedSourceClass {
#[must_use]
pub fn into_occurrence(self, range: Range) -> RejectedRegistryOccurrence {
RejectedRegistryOccurrence {
range,
reason: self.reason,
raw_value: self.raw_value,
declaration_key: self.declaration_key,
}
}
}
pub trait ParseResult: Send + Sync {
fn dependencies(&self) -> Vec<&dyn Dependency>;
fn workspace_root(&self) -> Option<&std::path::Path>;
fn uri(&self) -> &url::Url;
fn blocked_registries(&self) -> Vec<BlockedRegistryOccurrence> {
Vec::new()
}
fn selection_context(&self) -> SelectionContext {
SelectionContext::none()
}
fn invalid_minimum_stability(&self) -> Option<InvalidStabilityOccurrence> {
None
}
fn rejected_registries(&self) -> Vec<RejectedRegistryOccurrence> {
Vec::new()
}
fn as_any(&self) -> &dyn Any;
fn dependency_truncation(&self) -> Option<(usize, usize)> {
None
}
}
pub trait Dependency: Send + Sync {
fn name(&self) -> &crate::PackageName;
fn name_range(&self) -> Range;
fn version_requirement(&self) -> Option<&crate::VersionReq>;
fn version_range(&self) -> Option<Range>;
fn source(&self) -> crate::parser::DependencySource;
fn features(&self) -> &[String] {
&[]
}
fn features_range(&self) -> Option<Range> {
None
}
fn markers(&self) -> Option<&str> {
None
}
fn markers_range(&self) -> Option<Range> {
None
}
fn version_literal(&self) -> Option<&str> {
None
}
fn name_range_is_synthetic(&self) -> bool {
false
}
fn as_any(&self) -> &dyn Any;
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct EcosystemConfig {
pub show_up_to_date_hints: bool,
pub up_to_date_text: String,
pub needs_update_text: String,
pub loading_text: String,
pub show_loading_hints: bool,
pub network: crate::NetworkMode,
}
impl Default for EcosystemConfig {
fn default() -> Self {
Self::new()
}
}
impl EcosystemConfig {
#[must_use]
pub fn new() -> Self {
Self {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
network: crate::NetworkMode::Online,
}
}
#[must_use]
pub const fn with_show_up_to_date_hints(mut self, show_up_to_date_hints: bool) -> Self {
self.show_up_to_date_hints = show_up_to_date_hints;
self
}
#[must_use]
pub fn with_up_to_date_text(mut self, up_to_date_text: impl Into<String>) -> Self {
self.up_to_date_text = up_to_date_text.into();
self
}
#[must_use]
pub fn with_needs_update_text(mut self, needs_update_text: impl Into<String>) -> Self {
self.needs_update_text = needs_update_text.into();
self
}
#[must_use]
pub fn with_loading_text(mut self, loading_text: impl Into<String>) -> Self {
self.loading_text = loading_text.into();
self
}
#[must_use]
pub const fn with_show_loading_hints(mut self, show_loading_hints: bool) -> Self {
self.show_loading_hints = show_loading_hints;
self
}
#[must_use]
pub const fn with_network(mut self, network: crate::NetworkMode) -> Self {
self.network = network;
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LicenseSource {
#[default]
RegistryDeclaredSpdx,
FetchedDeclaredSpdx,
DetectedSpdx,
PomFreeText,
}
impl LicenseSource {
#[must_use]
pub const fn requires_dedicated_fetch(self) -> bool {
match self {
Self::RegistryDeclaredSpdx => false,
Self::FetchedDeclaredSpdx | Self::DetectedSpdx | Self::PomFreeText => true,
}
}
}
pub trait Ecosystem: Send + Sync + private::Sealed {
fn ecosystem_id(&self) -> EcosystemId;
fn id(&self) -> &'static str {
self.ecosystem_id().id()
}
fn display_name(&self) -> &'static str;
fn manifest_filenames(&self) -> &[&'static str];
fn manifest_extensions(&self) -> &[&'static str] {
&[]
}
fn manifest_patterns(&self) -> &[&'static str] {
&[]
}
fn manifest_directory_patterns(&self) -> &[(&'static str, &'static str)] {
&[]
}
fn lockfile_filenames(&self) -> &[&'static str] {
&[]
}
fn watched_config_filenames(&self) -> &[&'static str] {
&[]
}
fn routing_affecting_watched_configs(&self) -> &[&'static str] {
&[]
}
fn parse_manifest<'a>(
&'a self,
content: &'a str,
uri: &'a url::Url,
) -> BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>>;
fn parse_manifest_sync(&self, content: &str, uri: &url::Url) -> Option<Box<dyn ParseResult>> {
use futures::FutureExt;
self.parse_manifest(content, uri).now_or_never()?.ok()
}
fn registry(&self) -> Arc<dyn Registry>;
fn lockfile_provider(&self) -> Option<Arc<dyn crate::lockfile::LockFileProvider>> {
None
}
fn formatter(&self) -> &dyn EcosystemFormatter;
#[cfg(feature = "lsp-responses")]
fn generate_inlay_hints<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
versions: VersionData<'a>,
loading_state: crate::LoadingState,
config: &'a EcosystemConfig,
) -> BoxFuture<'a, Vec<InlayHint>> {
Box::pin(async move {
crate::lsp_helpers::generate_inlay_hints(
parse_result,
versions,
loading_state,
config,
self.formatter(),
)
})
}
#[cfg(feature = "lsp-responses")]
fn generate_hover<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
position: Position,
versions: VersionData<'a>,
freshness: crate::freshness::FreshnessSettings,
) -> BoxFuture<'a, Option<Hover>> {
Box::pin(async move {
let registry = self.registry();
crate::lsp_helpers::generate_hover(
parse_result,
position,
versions,
registry.as_ref(),
self.formatter(),
freshness,
crate::freshness::PublishTime::now(),
)
.await
})
}
#[cfg(feature = "lsp-responses")]
fn generate_code_actions<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
position: Position,
uri: &'a url::Url,
versions: VersionData<'a>,
content: &'a str,
) -> BoxFuture<'a, Vec<CodeAction>> {
Box::pin(async move {
let registry = self.registry();
crate::lsp_helpers::generate_code_actions(
parse_result,
position,
uri,
versions,
content,
registry.as_ref(),
self.formatter(),
)
.await
})
}
fn generate_diagnostics<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
versions: VersionData<'a>,
uri: &'a url::Url,
freshness: crate::freshness::FreshnessSettings,
severities: crate::lsp_helpers::DiagnosticSeverities,
) -> BoxFuture<'a, Vec<Diagnostic>> {
Box::pin(async move {
crate::lsp_helpers::generate_diagnostics_from_cache(
parse_result,
versions,
self.formatter(),
uri,
freshness,
severities,
crate::freshness::PublishTime::now(),
)
})
}
#[cfg(feature = "lsp-responses")]
fn generate_document_links(
&self,
_parse_result: &dyn ParseResult,
_uri: &url::Url,
) -> Vec<DocumentLink> {
Vec::new()
}
#[cfg(feature = "lsp-responses")]
fn generate_code_lenses<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
content: &'a str,
versions: VersionData<'a>,
uri: &'a url::Url,
command_id: &'a str,
) -> BoxFuture<'a, Vec<CodeLens>> {
Box::pin(async move {
crate::lsp_helpers::generate_code_lenses(
parse_result,
content,
versions,
self.formatter(),
uri,
command_id,
)
})
}
fn prepare_completions(&self) {}
#[cfg(feature = "lsp-responses")]
fn generate_completions<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
position: Position,
content: &'a str,
freshness: crate::FreshnessSettings,
) -> BoxFuture<'a, Completions> {
Box::pin(async move {
self.prepare_completions();
let request =
crate::completion::CompletionRequest::new(parse_result, position, freshness);
match crate::completion::detect_completion_context(parse_result, position, content) {
crate::completion::CompletionContext::PackageName { prefix, range } => self
.complete_package_name(request, prefix, range)
.await
.with_origin(crate::completion::CompletionOrigin::PackageName),
crate::completion::CompletionContext::Version {
package_name,
prefix,
} => self
.complete_version(request, package_name, prefix)
.await
.with_origin(crate::completion::CompletionOrigin::Version),
crate::completion::CompletionContext::Feature {
package_name,
prefix,
} => self
.complete_feature(request, package_name, prefix)
.await
.with_origin(crate::completion::CompletionOrigin::Feature),
crate::completion::CompletionContext::None => Completions::default(),
}
})
}
#[cfg(feature = "lsp-responses")]
fn complete_package_name<'a>(
&'a self,
_request: crate::completion::CompletionRequest<'a>,
_prefix: String,
_range: tower_lsp_server::ls_types::Range,
) -> BoxFuture<'a, Completions> {
Box::pin(std::future::ready(Completions::default()))
}
#[cfg(feature = "lsp-responses")]
fn version_operator_chars(&self) -> &'static [char] {
&[]
}
#[cfg(feature = "lsp-responses")]
fn complete_version<'a>(
&'a self,
request: crate::completion::CompletionRequest<'a>,
_package_name: crate::PackageName,
prefix: String,
) -> BoxFuture<'a, Completions> {
Box::pin(async move {
let registry = self.registry();
crate::completion::complete_versions_at_position(
registry.as_ref(),
self.formatter(),
request.parse_result,
request.position,
&prefix,
self.version_operator_chars(),
request.freshness,
)
.await
.into()
})
}
#[cfg(feature = "lsp-responses")]
fn complete_feature<'a>(
&'a self,
_request: crate::completion::CompletionRequest<'a>,
_package_name: crate::PackageName,
_prefix: String,
) -> BoxFuture<'a, Completions> {
Box::pin(std::future::ready(Completions::default()))
}
fn fallback_completion_prefix<'a>(
&self,
_content: &'a str,
_position: crate::position::Position,
) -> Option<&'a str> {
None
}
fn completion_insert_text(&self, metadata: &dyn Metadata) -> Option<String>;
fn fallback_completion_is_bare(
&self,
_content: &str,
_position: crate::position::Position,
) -> bool {
false
}
fn fallback_bare_insert_text(&self, metadata: &dyn Metadata) -> Option<String> {
Some(metadata.name().as_str().to_string())
}
fn package_search_is_incomplete(&self) -> bool {
false
}
fn fetch_license<'a>(
&'a self,
_name: &'a crate::PackageName,
_version: &'a crate::ConcreteVersion,
) -> BoxFuture<'a, Vec<String>> {
Box::pin(std::future::ready(Vec::new()))
}
fn license_source(&self) -> LicenseSource {
LicenseSource::RegistryDeclaredSpdx
}
fn as_any(&self) -> &dyn Any;
#[cfg(feature = "lsp-responses")]
fn collect_pin_all_to_sha_edits(
&self,
_parse_result: &dyn ParseResult,
_versions: VersionData<'_>,
) -> Vec<TextEdit> {
Vec::new()
}
#[cfg(feature = "lsp-responses")]
fn pin_all_to_sha_noun(&self) -> crate::lsp_helpers::PinNoun {
crate::lsp_helpers::PinNoun {
singular: "ref",
plural: "refs",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
#[test]
fn test_ecosystem_id_roundtrip() {
const ALL: &[EcosystemId] = &[
EcosystemId::Cargo,
EcosystemId::Npm,
EcosystemId::Pypi,
EcosystemId::Go,
EcosystemId::Bundler,
EcosystemId::Dart,
EcosystemId::Maven,
EcosystemId::Composer,
EcosystemId::Gradle,
EcosystemId::Swift,
EcosystemId::NuGet,
EcosystemId::Deno,
EcosystemId::GithubActions,
EcosystemId::GitlabCi,
];
for id in ALL {
let parsed: EcosystemId = id.id().parse().unwrap();
assert_eq!(parsed, *id);
assert_eq!(id.to_string(), id.id());
}
}
#[test]
fn test_osv_ecosystem_mapping_pinned() {
let expected: &[(EcosystemId, &str)] = &[
(EcosystemId::Cargo, "crates.io"),
(EcosystemId::Npm, "npm"),
(EcosystemId::Pypi, "PyPI"),
(EcosystemId::Go, "Go"),
(EcosystemId::Bundler, "RubyGems"),
(EcosystemId::Dart, "Pub"),
(EcosystemId::Maven, "Maven"),
(EcosystemId::Composer, "Packagist"),
(EcosystemId::Gradle, "Maven"),
(EcosystemId::Swift, "SwiftURL"),
(EcosystemId::NuGet, "NuGet"),
(EcosystemId::Deno, "npm"),
(EcosystemId::GithubActions, "GitHub Actions"),
];
for (id, expected_str) in expected {
assert_eq!(
id.osv_ecosystem().map(crate::osv::OsvEcosystem::as_str),
Some(*expected_str),
"unexpected OSV ecosystem string for {id:?}"
);
}
assert_eq!(EcosystemId::GitlabCi.osv_ecosystem(), None);
}
#[test]
fn test_ecosystem_id_from_str_unknown() {
let err = "unknown".parse::<EcosystemId>().unwrap_err();
assert_matches!(err, crate::error::DepsError::UnsupportedEcosystem(s) if s == "unknown");
}
#[test]
fn test_ecosystem_config_default() {
let config = EcosystemConfig::default();
assert!(config.show_up_to_date_hints);
assert_eq!(config.up_to_date_text, "✅");
assert_eq!(config.needs_update_text, "❌ {}");
}
#[test]
fn test_ecosystem_config_custom() {
let config = EcosystemConfig {
show_up_to_date_hints: false,
up_to_date_text: "OK".to_string(),
needs_update_text: "Update to {}".to_string(),
loading_text: "Loading...".to_string(),
show_loading_hints: false,
network: crate::NetworkMode::Online,
};
assert!(!config.show_up_to_date_hints);
assert_eq!(config.up_to_date_text, "OK");
assert_eq!(config.needs_update_text, "Update to {}");
}
#[test]
fn test_ecosystem_config_clone() {
let config1 = EcosystemConfig::default();
let config2 = config1.clone();
assert_eq!(config1.up_to_date_text, config2.up_to_date_text);
assert_eq!(config1.show_up_to_date_hints, config2.show_up_to_date_hints);
assert_eq!(config1.needs_update_text, config2.needs_update_text);
}
#[test]
fn test_dependency_default_features() {
struct MockDep;
impl Dependency for MockDep {
fn name(&self) -> &crate::PackageName {
static NAME: std::sync::LazyLock<crate::PackageName> =
std::sync::LazyLock::new(|| crate::PackageName::new("test"));
&NAME
}
fn name_range(&self) -> Range {
Range::default()
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
None
}
fn version_range(&self) -> Option<Range> {
None
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let dep = MockDep;
assert_eq!(dep.features(), &[] as &[String]);
}
struct StubDep;
impl Dependency for StubDep {
fn name(&self) -> &crate::PackageName {
static NAME: std::sync::LazyLock<crate::PackageName> =
std::sync::LazyLock::new(|| crate::PackageName::new("stub-dep"));
&NAME
}
fn name_range(&self) -> Range {
Range::default()
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
None
}
fn version_range(&self) -> Option<Range> {
None
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
struct StubParseResult {
uri: url::Url,
dep_count: usize,
}
impl ParseResult for StubParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
static STUB_DEP: StubDep = StubDep;
vec![&STUB_DEP as &dyn Dependency; self.dep_count]
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &url::Url {
&self.uri
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct StubEcosystem {
calling_thread: std::thread::ThreadId,
should_panic: bool,
dep_count: usize,
}
impl private::Sealed for StubEcosystem {}
impl Ecosystem for StubEcosystem {
fn id(&self) -> &'static str {
"stub"
}
fn ecosystem_id(&self) -> EcosystemId {
EcosystemId::Cargo
}
fn display_name(&self) -> &'static str {
"Stub"
}
fn manifest_filenames(&self) -> &[&'static str] {
&[]
}
fn parse_manifest<'a>(
&'a self,
_content: &'a str,
uri: &'a url::Url,
) -> BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>> {
Box::pin(async move {
assert!(!self.should_panic, "boom");
assert_ne!(
std::thread::current().id(),
self.calling_thread,
"parse must run on the blocking pool, not the calling thread"
);
Ok(Box::new(StubParseResult {
uri: uri.clone(),
dep_count: self.dep_count,
}) as Box<dyn ParseResult>)
})
}
fn registry(&self) -> Arc<dyn crate::Registry> {
unimplemented!()
}
fn formatter(&self) -> &dyn crate::lsp_helpers::EcosystemFormatter {
unimplemented!()
}
#[cfg(feature = "lsp-responses")]
fn generate_completions<'a>(
&'a self,
_parse_result: &'a dyn ParseResult,
_position: Position,
_content: &'a str,
_freshness: crate::FreshnessSettings,
) -> BoxFuture<'a, crate::completion::Completions> {
unimplemented!()
}
#[cfg(feature = "lsp-responses")]
fn complete_version<'a>(
&'a self,
_request: crate::completion::CompletionRequest<'a>,
_package_name: crate::PackageName,
_prefix: String,
) -> BoxFuture<'a, crate::completion::Completions> {
unimplemented!()
}
fn completion_insert_text(&self, _metadata: &dyn crate::Metadata) -> Option<String> {
unimplemented!()
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(feature = "lsp-responses")]
#[tokio::test]
async fn test_generate_completions_default_dispatch_stamps_origin_per_context() {
use crate::completion::CompletionOrigin;
struct OriginMockDep {
name: Range,
version: Range,
features: Range,
}
impl Dependency for OriginMockDep {
fn name(&self) -> &crate::PackageName {
static NAME: std::sync::LazyLock<crate::PackageName> =
std::sync::LazyLock::new(|| crate::PackageName::new("demo"));
&NAME
}
fn name_range(&self) -> Range {
self.name
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
static REQ: std::sync::LazyLock<crate::VersionReq> =
std::sync::LazyLock::new(|| crate::VersionReq::new("1.0"));
Some(&REQ)
}
fn version_range(&self) -> Option<Range> {
Some(self.version)
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn features_range(&self) -> Option<Range> {
Some(self.features)
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct OriginMockParseResult {
dep: OriginMockDep,
uri: url::Url,
}
impl ParseResult for OriginMockParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
vec![&self.dep]
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &url::Url {
&self.uri
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct OriginMockEcosystem;
impl private::Sealed for OriginMockEcosystem {}
impl Ecosystem for OriginMockEcosystem {
fn ecosystem_id(&self) -> EcosystemId {
EcosystemId::Cargo
}
fn display_name(&self) -> &'static str {
"origin-mock"
}
fn manifest_filenames(&self) -> &[&'static str] {
&[]
}
fn parse_manifest<'a>(
&'a self,
_content: &'a str,
_uri: &'a url::Url,
) -> BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>> {
Box::pin(async move { unimplemented!() })
}
fn registry(&self) -> Arc<dyn crate::Registry> {
unimplemented!()
}
fn formatter(&self) -> &dyn crate::lsp_helpers::EcosystemFormatter {
unimplemented!()
}
fn complete_version<'a>(
&'a self,
_request: crate::completion::CompletionRequest<'a>,
_package_name: crate::PackageName,
_prefix: String,
) -> BoxFuture<'a, Completions> {
Box::pin(std::future::ready(Completions::new(vec![])))
}
fn completion_insert_text(&self, _metadata: &dyn crate::Metadata) -> Option<String> {
unimplemented!()
}
fn as_any(&self) -> &dyn Any {
self
}
}
let content = "demo\n1.0\nderive\n";
let dep = OriginMockDep {
name: Range::new(
crate::position::Position::new(0, 0),
crate::position::Position::new(0, 4),
),
version: Range::new(
crate::position::Position::new(1, 0),
crate::position::Position::new(1, 3),
),
features: Range::new(
crate::position::Position::new(2, 0),
crate::position::Position::new(2, 6),
),
};
let parse_result = OriginMockParseResult {
dep,
uri: crate::test_util::test_uri("/test/manifest.toml"),
};
let eco = OriginMockEcosystem;
let freshness = crate::FreshnessSettings::default();
for (position, expected) in [
(Position::new(0, 2), CompletionOrigin::PackageName),
(Position::new(1, 1), CompletionOrigin::Version),
(Position::new(2, 3), CompletionOrigin::Feature),
(Position::new(3, 0), CompletionOrigin::Unresolved),
] {
let result = eco
.generate_completions(&parse_result, position, content, freshness)
.await;
assert_eq!(
result.origin, expected,
"position {position:?} expected origin {expected:?}, got {:?}",
result.origin
);
}
}
#[tokio::test]
async fn test_parse_manifest_blocking_runs_parse_off_calling_thread() {
let ecosystem: Arc<dyn Ecosystem> = Arc::new(StubEcosystem {
calling_thread: std::thread::current().id(),
should_panic: false,
dep_count: 0,
});
let uri = crate::test_util::test_uri("/test/manifest.toml");
let parsed = parse_manifest_blocking(&ecosystem, "content", &uri)
.await
.unwrap();
assert_eq!(parsed.uri(), &uri);
}
#[tokio::test]
async fn test_parse_manifest_blocking_caps_dependencies_over_the_ceiling() {
let ecosystem: Arc<dyn Ecosystem> = Arc::new(StubEcosystem {
calling_thread: std::thread::current().id(),
should_panic: false,
dep_count: crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT + 1,
});
let uri = crate::test_util::test_uri("/test/manifest.toml");
let parsed = parse_manifest_blocking(&ecosystem, "content", &uri)
.await
.unwrap();
assert_eq!(
parsed.dependencies().len(),
crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT
);
assert_eq!(
parsed.dependency_truncation(),
Some((
crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT,
crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT + 1
))
);
}
#[tokio::test]
async fn test_parse_manifest_blocking_panic_becomes_parse_error() {
let ecosystem: Arc<dyn Ecosystem> = Arc::new(StubEcosystem {
calling_thread: std::thread::current().id(),
should_panic: true,
dep_count: 0,
});
let uri = crate::test_util::test_uri("/test/manifest.toml");
let Err(err) = parse_manifest_blocking(&ecosystem, "content", &uri).await else {
panic!("expected parse_manifest_blocking to return an error");
};
match err {
crate::error::DepsError::ParseError { file_type, source } => {
assert!(file_type.contains("manifest at"));
assert!(
source.to_string().contains("boom"),
"panic message should be preserved in the error source, got: {source}"
);
}
other => panic!("Expected ParseError, got: {other:?}"),
}
}
#[tokio::test]
async fn test_parse_manifest_now_caps_dependencies_over_the_ceiling() {
let foreign_thread = std::thread::spawn(|| std::thread::current().id())
.join()
.unwrap();
let ecosystem = StubEcosystem {
calling_thread: foreign_thread,
should_panic: false,
dep_count: crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT + 1,
};
let uri = crate::test_util::test_uri("/test/manifest.toml");
let parsed = parse_manifest_now(&ecosystem, "content", &uri)
.expect("an immediately-ready parse must resolve on the first poll");
assert_eq!(
parsed.dependencies().len(),
crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT
);
assert_eq!(
parsed.dependency_truncation(),
Some((
crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT,
crate::dependency_cap::MAX_DEPENDENCIES_PER_DOCUMENT + 1
))
);
}
#[tokio::test]
async fn test_parse_manifest_now_returns_none_for_a_pending_future() {
struct PendingEcosystem;
impl private::Sealed for PendingEcosystem {}
impl Ecosystem for PendingEcosystem {
fn ecosystem_id(&self) -> EcosystemId {
EcosystemId::Composer
}
fn display_name(&self) -> &'static str {
"pending-stub"
}
fn manifest_filenames(&self) -> &[&'static str] {
&[]
}
fn parse_manifest<'a>(
&'a self,
_content: &'a str,
_uri: &'a url::Url,
) -> BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>> {
Box::pin(std::future::pending())
}
fn registry(&self) -> Arc<dyn crate::Registry> {
unimplemented!()
}
fn formatter(&self) -> &dyn crate::lsp_helpers::EcosystemFormatter {
unimplemented!()
}
fn complete_version<'a>(
&'a self,
_request: crate::completion::CompletionRequest<'a>,
_package_name: crate::PackageName,
_prefix: String,
) -> BoxFuture<'a, Completions> {
unimplemented!()
}
fn completion_insert_text(&self, _metadata: &dyn crate::Metadata) -> Option<String> {
unimplemented!()
}
fn as_any(&self) -> &dyn Any {
self
}
}
let ecosystem = PendingEcosystem;
let uri = crate::test_util::test_uri("/test/manifest.toml");
assert!(parse_manifest_now(&ecosystem, "content", &uri).is_none());
}
#[test]
fn test_parse_manifest_now_returns_none_outside_a_tokio_runtime() {
let ecosystem: Arc<dyn Ecosystem> = Arc::new(StubEcosystem {
calling_thread: std::thread::current().id(),
should_panic: false,
dep_count: 0,
});
let uri = crate::test_util::test_uri("/test/manifest.toml");
assert!(
tokio::runtime::Handle::try_current().is_err(),
"this test must run with no ambient tokio runtime for the assertion below to mean \
anything"
);
assert!(parse_manifest_now(ecosystem.as_ref(), "content", &uri).is_none());
}
crate::debug_redaction_conformance!(
test_blocked_registry_occurrence_debug_redacts_credentials,
2,
BlockedRegistryOccurrence {
range: Range::default(),
class: crate::net_policy::HostClass::Loopback,
raw_value: crate::conformance::CREDENTIAL_PROBE_URL.to_string(),
declaration_key: crate::conformance::CREDENTIAL_PROBE_KEY.to_string(),
},
);
crate::debug_redaction_conformance!(
test_blocked_source_class_debug_redacts_credentials,
2,
BlockedSourceClass {
class: crate::net_policy::HostClass::Loopback,
raw_value: crate::conformance::CREDENTIAL_PROBE_URL.to_string(),
declaration_key: crate::conformance::CREDENTIAL_PROBE_KEY.to_string(),
},
);
}