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
24 changes: 24 additions & 0 deletions documentation/validation/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,30 @@ Do not edit `code/common/info-description-templates.yaml` in the API repository.

</details>

<a name="p-040-component-renaming-conflict"></a>
<details>
<summary>[P-040] Why does bundling rename a component I didn't touch?</summary>

Applies to: `[P-040] Component-renaming collision during bundling`

Two different components — one defined locally, one pulled in from a common file via an
external `$ref` — are competing for the same name with different content. When the spec is
bundled, the local definition keeps the name; the externally-referenced one is renamed to
`<Name>-2`, and every use site that resolved to it is silently repointed at the renamed copy.
This is not a bundler quirk to ignore: a reader of the bundled spec sees the wrong schema at
those use sites.

This only fires for a genuine conflict — same name, **different** content. A local component
that intentionally proxies a common one under the same name, with identical content, is not
flagged.

**What you do:** rename the *local* definition to a distinct, API-specific name — you can't
rename the common definition, since Commonalities owns it. If the local definition isn't
actually needed (it has no reason to diverge from the common one), delete it and reference the
common component directly instead.

</details>

## Related

- [Validation problem messages](problem-messages.md) — the shape of each message
Expand Down
6 changes: 6 additions & 0 deletions validation/engines/python_checks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ._types import CheckDescriptor, CheckScope

from .bundling_checks import check_component_renaming_conflict
from .error_code_checks import check_conflict_deprecated, check_contextcode_format
from .externaldocs_checks import check_externaldocs
from .filename_checks import check_filename_kebab_case, check_filename_matches_api_name
Expand Down Expand Up @@ -71,6 +72,11 @@
CheckScope.API,
check_info_description_templates,
),
CheckDescriptor(
"check-component-renaming-conflict",
CheckScope.API,
check_component_renaming_conflict,
),
CheckDescriptor("check-externaldocs-repository", CheckScope.API, check_externaldocs),
# --- Repo-level checks (run once) ---
CheckDescriptor("check-test-directory-exists", CheckScope.REPO, check_test_directory_exists),
Expand Down
113 changes: 113 additions & 0 deletions validation/engines/python_checks/bundling_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Bundler component-renaming conflict check.

Runs Redocly's bundler with ``--component-renaming-conflicts-severity=error``
against the current API definition to detect a name shared between a local
component and a different-content component pulled in via an external
``$ref``. Redocly silently renames the loser to ``<Name>-2``; this only
reports the cases where the two definitions actually differ (a same-content
proxy/alias is never flagged).
"""

from __future__ import annotations

import os
import re
import subprocess
import tempfile
from pathlib import Path
from typing import List

from validation.context import ValidationContext

from ._types import make_finding

_ENGINE_RULE = "check-component-renaming-conflict"
_EXECUTION_ERROR_RULE = "component-renaming-conflict-execution-error"
_EXTERNAL_REF_RE = re.compile(r'\$ref\s*:\s*["\']?\.\.')
_CONFLICT_RE = re.compile(
r'\[\d+\]\s+\S+:\d+:\d+\s+at\s+\S+\s*\n+'
r"Two schemas are referenced with the same name but different content\. "
r'Renamed (?P<name>\S+) to \S+-2\.'
)
_TIMEOUT_SECONDS = 60


def check_component_renaming_conflict(
repo_path: Path, context: ValidationContext
) -> List[dict]:
"""Detect bundler component-renaming collisions for the current API.

API-scoped check — the adapter calls this once per API context.
"""
if not context.apis:
return []

api = context.apis[0]
spec_file = api.spec_file or f"code/API_definitions/{api.api_name}.yaml"
full_path = repo_path / spec_file
if not full_path.is_file():
return []

content = full_path.read_text(encoding="utf-8")
if not _EXTERNAL_REF_RE.search(content):
return []

with tempfile.TemporaryDirectory() as tmp:
try:
result = subprocess.run(
[
"redocly", "bundle", spec_file,
"--component-renaming-conflicts-severity=error",
"-o", str(Path(tmp) / "bundled.yaml"),
],
cwd=repo_path,
capture_output=True,
text=True,
timeout=_TIMEOUT_SECONDS,
check=False,
# GitHub Actions sets GITHUB_ACTIONS unconditionally, which
# redocly's color library treats as reason enough to force
# ANSI color into captured (non-TTY) output; NO_COLOR
# suppresses it. Verified against a real Actions run.
env={**os.environ, "NO_COLOR": "1"},
)
except (FileNotFoundError, OSError, subprocess.TimeoutExpired) as exc:
return [
make_finding(
engine_rule=_EXECUTION_ERROR_RULE,
level="error",
message=f"Component-renaming-conflict probe could not run: {exc}",
path=spec_file,
line=1,
api_name=api.api_name,
)
]

if result.returncode == 0:
return []

output = result.stdout + result.stderr
matches = list(_CONFLICT_RE.finditer(output))
if not matches:
return []

return [
make_finding(
engine_rule=_ENGINE_RULE,
level="error",
message=(
f"Bundling collides on the name '{m.group('name')}': the local "
f"definition keeps it, and the different-content component "
f"pulled in via an external $ref is silently renamed to "
f"'{m.group('name')}-2' at its use sites. Give the local "
f"'{m.group('name')}' a distinct, API-specific name — the "
f"common definition isn't yours to rename. If it isn't "
f"actually needed locally, reference the common component "
f"directly instead."
),
path=spec_file,
line=1,
api_name=api.api_name,
)
for m in matches
]
26 changes: 26 additions & 0 deletions validation/rules/python-rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -628,3 +628,29 @@
suggestion: >-
Set externalDocs.description to exactly "Product documentation at
CAMARA".

# P-040: check-component-renaming-conflict
# Redocly's bundler silently disambiguates when a local component and a
# different-content component pulled in via an external $ref compete for the
# same name: the local definition keeps the name, the external one is
# renamed to <Name>-2 and every use site that resolved to it is repointed.
# A same-name-same-content proxy/alias is never flagged. error only at
# public, warn everywhere else — deliberately skips both the "new
# requirement" warn-only ramp and the initial-API (0.x) relaxation: the fix
# has no wire-format/backward-compatibility cost, so there is nothing for
# either convention to protect.
- id: P-040
engine: python
engine_rule: check-component-renaming-conflict
short_title: "Component-renaming collision during bundling"
documentation_url: "https://github.com/camaraproject/tooling/blob/main/documentation/validation/faq.md#p-040-component-renaming-conflict"
conditional_level:
default: error
overrides:
- condition:
target_api_status: [draft, alpha, rc]
level: warn
suggestion: >-
Give the local component a distinct, API-specific name — the common
definition isn't yours to rename. If it isn't actually needed locally,
reference the common component directly instead.
4 changes: 2 additions & 2 deletions validation/rules/rule-inventory.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ version: 1
generated: 2026-04-07

summary:
total_implemented: 146
total_implemented: 149
total_gap: 0
total_manual: 25
total_pending: 0
total_tested: 95
by_engine:
spectral: 85
gherkin: 25
python: 23
python: 26
yamllint: 13

# ---------------------------------------------------------------------------
Expand Down
Loading