diff --git a/sphinx_needs/schema/config_utils.py b/sphinx_needs/schema/config_utils.py index 6f5c50889..c0f302033 100644 --- a/sphinx_needs/schema/config_utils.py +++ b/sphinx_needs/schema/config_utils.py @@ -241,51 +241,68 @@ def resolve_refs( curr_item: Any, circular_refs_guard: set[str], is_defs: bool = False, + path: str = "", ) -> None: - """Recursively resolve and replace $ref entries in schemas.""" + """Recursively resolve and replace $ref entries in schemas. + + ``path`` accumulates the JSON location of ``curr_item`` from the root of + ``needs_schema_definitions`` (e.g. ``schemas[0].validate.network.links.contains.local`` + or ``$defs.safe-impl.allOf[1]``) and is appended to ``$ref`` error messages so a + faulty reference can be located without hunting through the whole config. + """ + loc = f" (at {path})" if path else "" if isinstance(curr_item, dict): if "$ref" in curr_item: if len(curr_item) == 1: ref_raw = curr_item["$ref"] if not isinstance(ref_raw, str): raise NeedsConfigException( - f"Invalid $ref value: {ref_raw}, expected a string." + f"Invalid $ref value: {ref_raw}, expected a string{loc}." ) if not ref_raw.startswith("#/$defs/"): raise NeedsConfigException( - f"Invalid $ref value: {ref_raw}, expected to start with '#/$defs/'." + f"Invalid $ref value: {ref_raw}, expected to start with '#/$defs/'{loc}." ) ref = ref_raw[8:] # Remove '#/$defs/' prefix if ref not in defs: raise NeedsConfigException( - f"Reference '{ref}' not found in definitions." + f"Reference '{ref}' not found in definitions{loc}." ) if ref in circular_refs_guard: raise NeedsConfigException( - f"Circular reference detected for '{ref}'." + f"Circular reference detected for '{ref}'{loc}." ) circular_refs_guard.add(ref) curr_item.clear() curr_item.update(defs[ref]) - resolve_refs(defs, curr_item, circular_refs_guard) + resolve_refs(defs, curr_item, circular_refs_guard, path=path) circular_refs_guard.remove(ref) else: raise NeedsConfigException( - f"Invalid $ref entry, expected a single $ref key: {curr_item}" + f"Invalid $ref entry, expected a single $ref key: {curr_item}{loc}" ) for key, value in curr_item.items(): + child_path = f"{path}.{key}" if path else str(key) if isinstance(value, dict): if is_defs: circular_refs_guard.add(key) - resolve_refs(defs, value, circular_refs_guard, is_defs=(key == "$defs")) + resolve_refs( + defs, + value, + circular_refs_guard, + is_defs=(key == "$defs"), + path=child_path, + ) if is_defs: circular_refs_guard.remove(key) if isinstance(value, list): - for item in value: - resolve_refs(defs, item, circular_refs_guard) + for idx, item in enumerate(value): + resolve_refs( + defs, item, circular_refs_guard, path=f"{child_path}[{idx}]" + ) elif isinstance(curr_item, list): - for item in curr_item: - resolve_refs(defs, item, circular_refs_guard) + for idx, item in enumerate(curr_item): + resolve_refs(defs, item, circular_refs_guard, path=f"{path}[{idx}]") def populate_field_type( diff --git a/tests/schema/__snapshots__/test_schema.ambr b/tests/schema/__snapshots__/test_schema.ambr index e8eca35bb..14e4dfd24 100644 --- a/tests/schema/__snapshots__/test_schema.ambr +++ b/tests/schema/__snapshots__/test_schema.ambr @@ -579,19 +579,19 @@ "Schema '[0]' defines an unknown network link type 'links2'." # --- # name: test_schema_config[ref_error] - "Reference 'not-exist' not found in definitions." + "Reference 'not-exist' not found in definitions (at schemas[0].validate.local)." # --- # name: test_schema_config[ref_mixed_error] - "Invalid $ref entry, expected a single $ref key: {'$ref': '#/$defs/not-exist', 'required': ['type']}" + "Invalid $ref entry, expected a single $ref key: {'$ref': '#/$defs/not-exist', 'required': ['type']} (at schemas[0].validate.local)" # --- # name: test_schema_config[ref_not_defs_prefix] - "Invalid $ref value: type-impl, expected to start with '#/$defs/'." + "Invalid $ref value: type-impl, expected to start with '#/$defs/' (at schemas[0].validate.local)." # --- # name: test_schema_config[ref_recursive_error] - "Circular reference detected for 'type-impl'." + "Circular reference detected for 'type-impl' (at $defs.type-impl.allOf[0])." # --- # name: test_schema_config[ref_value_not_string] - 'Invalid $ref value: 123, expected a string.' + 'Invalid $ref value: 123, expected a string (at schemas[0].validate.local).' # --- # name: test_schema_config[schemas_select_pattern_unsafe_error] '''