Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/tombi-extension/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ impl FromLsp<CompletionContent> for tower_lsp::lsp_types::CompletionItem {
) -> tower_lsp::lsp_types::CompletionItem {
const SECTION_SEPARATOR: &str = "-----";

let sorted_text = format!("{}_{}", source.priority.as_prefix(), &source.label);
let sorted_text = format!("{}_{}", source.priority.as_prefix(), source.label);

let mut schema_text = None;
if let Some(schema_uri) = &source.schema_uri {
Expand Down
2 changes: 2 additions & 0 deletions crates/tombi-linter/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ mod prefix_items_test_schema;
mod recursive_anchor_ref_test_schema;
#[path = "integration/recursive_defs_any_of_test_schema.rs"]
mod recursive_defs_any_of_test_schema;
#[path = "integration/schema_resolution_error.rs"]
mod schema_resolution_error;
#[path = "integration/string_format_test_schema.rs"]
mod string_format_test_schema;
#[path = "integration/table_const_enum_test_schema.rs"]
Expand Down
241 changes: 241 additions & 0 deletions crates/tombi-linter/tests/integration/schema_resolution_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
use tombi_diagnostic::Level;
use tombi_linter::test_lint;

fn schema_path() -> std::path::PathBuf {
tombi_test_lib::project_root_path()
.join("schemas")
.join("schema-resolution-error-test.schema.json")
}

test_lint! {
#[test]
fn test_property_schema_resolution_error(
r#"
direct = 1
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_all_of_schema_resolution_error(
r#"
all-of-error = 1
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_any_of_schema_resolution_error(
r#"
any-of-error = 1
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_one_of_schema_resolution_error(
r#"
one-of-error = 1
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_if_schema_resolution_error(
r#"
if-error = {}
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_then_schema_resolution_error(
r#"
then-error = {}
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_else_schema_resolution_error(
r#"
else-error = {}
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_items_schema_resolution_error(
r#"
items-error = [1]
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_prefix_items_schema_resolution_error(
r#"
prefix-items-error = [1]
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_overflow_items_schema_resolution_error(
r#"
overflow-items-error = [1, 2]
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_overflow_items_schema_not_applied(
r#"
overflow-items-error = [1]
"#,
SchemaPath(schema_path()),
) -> Ok(_)
}

test_lint! {
#[test]
fn test_contains_schema_resolution_error(
r#"
contains-error = [1]
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_unevaluated_items_schema_resolution_error(
r#"
unevaluated-items-error = [1]
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_pattern_property_schema_resolution_error(
r#"
[pattern-properties-error]
pat-key = 1
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_additional_property_schema_resolution_error(
r#"
[additional-properties-error]
extra = 1
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_unevaluated_property_schema_resolution_error(
r#"
[unevaluated-properties-error]
extra = 1
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR },
{ code: "table-strict-additional-keys", level: Level::WARNING }
])
}

test_lint! {
#[test]
fn test_property_names_schema_resolution_error(
r#"
[property-names-error]
key = 1
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_dependency_schema_resolution_error(
r#"
[dependencies-error]
trigger = "x"
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}

test_lint! {
#[test]
fn test_dependent_schema_resolution_error(
r#"
[dependent-schemas-error]
trigger = "x"
"#,
SchemaPath(schema_path()),
) -> Diagnostics([
{ code: "invalid-json-pointer", level: Level::ERROR }
])
}
4 changes: 2 additions & 2 deletions crates/tombi-lsp/src/completion/value/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ impl FindCompletionContents for tombi_document_tree::Table {
{
log::trace!(
"property_schema = {:?}",
&current_schema.value_schema
current_schema.value_schema
);

let Some(mut contents) =
Expand Down Expand Up @@ -311,7 +311,7 @@ impl FindCompletionContents for tombi_document_tree::Table {
if pattern.is_match(accessor_str) {
log::trace!(
"pattern_property_schema = {:?}",
&current_schema.value_schema
current_schema.value_schema
);
if let Ok(Some(current_schema)) = table_schema
.resolve_pattern_property_schema(
Expand Down
4 changes: 2 additions & 2 deletions crates/tombi-schema-store/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ pub use not_schema::NotSchema;
pub use offset_date_time_schema::OffsetDateTimeSchema;
pub use one_of_schema::OneOfSchema;
pub use referable_schema::{
CurrentSchema, Referable, is_online_url, resolve_and_collect_schemas, resolve_json_pointer,
resolve_schema_item,
CurrentSchema, Referable, is_online_url, resolve_and_collect_schemas,
resolve_and_collect_schemas_with_errors, resolve_json_pointer, resolve_schema_item,
};
pub use schema_context::{ResolvedFormatOrder, SchemaContext};
pub use schema_cycle_guard::{SchemaCycleGuard, SchemaVisits};
Expand Down
42 changes: 35 additions & 7 deletions crates/tombi-schema-store/src/schema/referable_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,19 +692,45 @@ fn is_plain_name_fragment(fragment: &str) -> bool {
!fragment.is_empty() && !fragment.contains('/')
}

pub async fn resolve_and_collect_schemas(
schemas: &super::ReferableValueSchemas,
schema_uri: Cow<'_, SchemaUri>,
definitions: Cow<'_, SchemaDefinitions>,
schema_store: &crate::SchemaStore,
schema_visits: &crate::SchemaVisits,
accessors: &[crate::Accessor],
) -> Option<Vec<CurrentSchema<'static>>> {
let (collected, errors) = resolve_and_collect_schemas_with_errors(
schemas,
schema_uri,
definitions,
schema_store,
schema_visits,
accessors,
)
.await?;

for err in errors {
log::warn!("{err}");
}

Some(collected)
}

/// Two-path schema collection: tries a read lock first for already-resolved schemas,
/// resolves refs on cloned entries, and writes back only newly-resolved entries.
///
/// Returns the successfully resolved schemas together with any resolution errors.
/// Returns `None` when schema traversal is re-entrant (cycle guard) or when
/// an initial read lock cannot be acquired due to concurrent mutation.
pub async fn resolve_and_collect_schemas(
pub async fn resolve_and_collect_schemas_with_errors(
schemas: &super::ReferableValueSchemas,
schema_uri: Cow<'_, SchemaUri>,
definitions: Cow<'_, SchemaDefinitions>,
schema_store: &crate::SchemaStore,
schema_visits: &crate::SchemaVisits,
accessors: &[crate::Accessor],
) -> Option<Vec<CurrentSchema<'static>>> {
) -> Option<(Vec<CurrentSchema<'static>>, Vec<crate::Error>)> {
let Some(_cycle_guard) = schema_visits.get_cycle_guard(schemas) else {
log::debug!(
"detected composite schema cycle while collecting schemas: schema_uri={schema_uri} accessors={accessors} reason=reentrant_schema_traversal",
Expand Down Expand Up @@ -749,6 +775,7 @@ pub async fn resolve_and_collect_schemas(
// Build output from read result and avoid cloning the whole referable vector.
if let Some(resolved_schemas) = resolved_schemas {
let mut collected = Vec::with_capacity(resolved_schemas.len());
let mut errors = Vec::new();
let default_schema_uri = schema_uri.as_ref().clone();
let default_definitions = definitions.clone().into_owned();

Expand All @@ -765,7 +792,7 @@ pub async fn resolve_and_collect_schemas(
),
Ok(None) => (default_schema_uri.clone(), default_definitions.clone()),
Err(err) => {
log::warn!("{err}");
errors.push(err);
continue;
}
}
Expand All @@ -780,11 +807,12 @@ pub async fn resolve_and_collect_schemas(
});
}

return Some(collected);
return Some((collected, errors));
}

// Slow path: unresolved refs exist. Resolve on cloned entries and cache back.
let mut collected = Vec::with_capacity(schema_entries.len());
let mut errors = Vec::new();
let mut resolved_indices = Vec::new();
for (index, referable_schema) in schema_entries.iter_mut().enumerate() {
let was_ref = referable_schema.is_ref();
Expand All @@ -795,7 +823,7 @@ pub async fn resolve_and_collect_schemas(
Ok(Some(current_schema)) => collected.push(current_schema.into_owned()),
Ok(None) => {}
Err(err) => {
log::warn!("{err}");
errors.push(err);
}
}

Expand All @@ -812,7 +840,7 @@ pub async fn resolve_and_collect_schemas(
schema_uri = schema_uri.as_ref(),
accessors = crate::Accessors::from(accessors.to_vec())
);
return Some(collected);
return Some((collected, errors));
};

for index in resolved_indices {
Expand All @@ -826,7 +854,7 @@ pub async fn resolve_and_collect_schemas(
}
}

Some(collected)
Some((collected, errors))
}

/// Resolve a schema item without holding its write lock across await points.
Expand Down
Loading
Loading