Skip to content

Conversation

@jbrinkman
Copy link
Owner

@jbrinkman jbrinkman commented Aug 8, 2025

Summary

This PR enhances the diagnostic capabilities of the ApiComparison class by adding detailed validation error messages. Previously, when API comparisons failed validation, users only received a generic "API comparison is invalid" message with no indication of the specific problem.

Changes Made

1. Added InvalidMessage Property to ApiComparison

  • New read-only property that stores detailed validation error information
  • Defaults to empty string when comparison is valid
  • Provides specific details about what failed validation

2. Enhanced IsValid() Method

  • Now provides specific error messages for each validation failure type:
    • Invalid individual changes: Lists specific changes that fail their own validation with descriptions and types
    • Wrong collection placement: Identifies changes placed in wrong collections (e.g., Added changes in Removals collection)
  • Clears InvalidMessage at start of each validation run
  • Uses detailed LINQ queries to identify specific problematic items

3. Updated ExitCodeManager Logging

  • Modified to include the detailed InvalidMessage in log output when available
  • Falls back to generic message if InvalidMessage is empty
  • Provides much better debugging information for validation failures

Example Error Messages

Before

[2025-08-08 15:43:36] warn: DotNetApiDiff.ExitCodes.ExitCodeManager[0]
      API comparison is invalid, returning ComparisonError exit code

After

[2025-08-08 15:43:36] warn: DotNetApiDiff.ExitCodes.ExitCodeManager[0]
      API comparison is invalid: Additions collection contains 1 change(s) with wrong type: 'Some change' (Modified), returning ComparisonError exit code

Or:

[2025-08-08 15:43:36] warn: DotNetApiDiff.ExitCodes.ExitCodeManager[0]
      API comparison is invalid: Found 2 invalid change(s): 'Added method Foo' (Added), 'Removed property Bar' (Removed), returning ComparisonError exit code

Benefits

  • 🔍 Better diagnostics: Immediate identification of specific validation failures
  • ⚡ Faster debugging: No more guessing what went wrong with comparison validation
  • 👨‍💻 Improved developer experience: Clear, actionable error messages
  • 🔧 Easier troubleshooting: Helps users fix comparison issues quickly

Testing

  • ✅ All existing tests continue to pass
  • ✅ No breaking changes to public API
  • ✅ Backwards compatible (InvalidMessage defaults to empty string)
  • ✅ No impact on existing functionality

Files Changed

  • src/DotNetApiDiff/Models/ApiComparison.cs - Added InvalidMessage property and enhanced validation
  • src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs - Updated logging to include detailed error messages

Related Issues

Resolves #51

Type of Change

  • 🚀 New feature (non-breaking change which adds functionality)
  • 🐛 Bug fix (addresses diagnostic gap)
  • 📖 Documentation update (improves error messaging)

This enhancement significantly improves the debugging experience when API comparisons fail validation, making it much easier to identify and resolve issues.

Summary by CodeRabbit

  • New Features

    • API difference reports now group changes by containing type, offering clearer organization and summaries for each type, including counts and breaking change indicators.
    • Added collapsible sections for type groups in the HTML report, with state persistence across sessions for improved navigation.
  • Enhancements

    • Improved error messages for invalid API comparisons, providing detailed validation feedback.
    • HTML reports feature richer metadata, badges for breaking changes, and more interactive headers and toggles.
    • Updated CSS for a modern, type-centric layout and enhanced visual clarity.
    • Refined logging messages to include detailed invalid comparison information.
  • Bug Fixes

    • Adjusted CLI workflow tests to expect the correct exit codes for breaking changes.
  • Tests

    • Updated test assertions to reflect new group header formats and improved validation messaging.

- Implement hierarchical change category organization with collapsible type groups
- Change from type-first to change-category-first organization
- Add collapsible type groups that default to collapsed state
- Fix summary navigation links to properly navigate to sections
- Update HTML template structure:
  * Modify HtmlFormatterScriban.cs to use GroupChangesByType method
  * Update main-layout.scriban with collapsible type groups and navigation
  * Enhance styles.css with type group collapsibility and hover effects
  * Add scripts.js toggle functionality with session storage persistence
- Make section headers fully clickable (not just arrow buttons)
- Add consistent hover effects for section and type headers
- Fix CLI workflow tests to expect correct exit codes
- All 425 tests passing

Resolves #49
- Add InvalidMessage property to ApiComparison class for detailed error reporting
- Enhanced IsValid() method to provide specific validation failure details
- Updated ExitCodeManager to include InvalidMessage in log output
- Improves debugging experience when API comparisons fail validation

Resolves #51
@coderabbitai
Copy link

coderabbitai bot commented Aug 8, 2025

Walkthrough

The changes introduce detailed validation error reporting for API comparisons. An InvalidMessage property is added to ApiComparison, and the IsValid() method now records specific validation errors. The exit code manager logs these details when validation fails. No public API is broken, and only targeted files are affected.

Changes

Cohort / File(s) Change Summary
Validation Error Messaging
src/DotNetApiDiff/Models/ApiComparison.cs, src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs
Introduced InvalidMessage property to ApiComparison, enhanced IsValid() to capture and report specific validation errors, and updated exit code manager to log these details when validation fails.
Test Adjustments for Validation
tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs
Updated CLI workflow tests to expect exit code 1 instead of 2 for breaking changes, aligning with revised validation logic.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ApiComparison
    participant ExitCodeManager
    participant Logger

    User->>ApiComparison: IsValid()
    ApiComparison->>ApiComparison: Validate changes, set InvalidMessage if invalid
    ApiComparison-->>User: Return true/false

    User->>ExitCodeManager: GetExitCode(apiComparison)
    ExitCodeManager->>ApiComparison: Check IsValid()
    alt Invalid
        ExitCodeManager->>Logger: Log InvalidMessage (if present)
        ExitCodeManager-->>User: Return ComparisonError
    else Valid
        ExitCodeManager-->>User: Return appropriate exit code
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~7 minutes

Assessment against linked issues

Objective Addressed Explanation
Add InvalidMessage property to ApiComparison (#51)
Enhance IsValid() to provide specific error messages for each validation failure type (#51)
Update ExitCodeManager logging to include detailed InvalidMessage in output (#51)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes found.

Poem

A rabbit hopped through code so neat,
Now errors speak, not just retreat!
With messages clear, no more guess or grope,
Debugging’s a breeze, there’s reason for hope.
🐇💡
“Invalid!” they say, but now with a clue—
The garden of code grows easier for you!

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-51-api-comparison-validation-diagnostics

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (9)
src/DotNetApiDiff/Models/ApiComparison.cs (1)

84-113: Deduplicate the four “wrong-collection” checks

The four almost-identical LINQ blocks differ only by the collection & expected ChangeType.
Extract into a small local function to cut repetition and reduce future maintenance risk.

bool CheckWrongType(IEnumerable<ApiChange> set, ChangeType expected, string setName)
{
    var wrong = set.Where(c => c.Type != expected).ToList();
    if (!wrong.Any()) return true;

    messages.Add($"{setName} contains {wrong.Count} invalid item(s): " +
                 string.Join(", ", wrong.Select(c => $"'{c.Description}' ({c.Type})")));
    return false;
}
src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs (2)

235-254: GroupChanges now appears unused – safe to remove

After the switch to GroupChangesByType, the old GroupChanges method is dead code.
Keeping it around inflates the file and invites drift. Consider deleting it (or marking Obsolete if public).


285-333: Edge-cases missing in ExtractContainingType & friends

String parsing fails for some legal CLR names, e.g. generic arity/back-tick (TypeName\1.Method) or operators (op_Addition). Relying on the Roslyn symbol model (or at least MetadataName) would be safer than manual LastIndexOf`.
If that’s out of scope, add unit tests to cover:

  • Generic types & members (List<T>.Add)
  • Nested generic types (Outer\1+Inner`)
  • Overloaded operators (MyType.op_Addition)
src/DotNetApiDiff/Reporting/HtmlTemplates/type-group.scriban (1)

24-31: Consider adding accessible text for the visual-only glyphs

The template relies on emoji (📁, ⚠️, BREAKING) to convey key information. Screen-reader users will not get that context. Add visually-hidden text (e.g., <span class="sr-only">Folder</span>) or use aria-label on the elements so the semantics aren’t lost.

src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js (1)

140-170: Section initialisation list is hard-coded

initializeSections() enumerates only four section IDs. If additional categories (e.g., moved-items) are added later they will not get the collapsed/expanded behaviour. Iterate over .changes-section elements instead of a static array.

src/DotNetApiDiff/Reporting/HtmlTemplates/styles.css (1)

107-118: Duplicate .toggle-icon rotation rules

.toggle-icon rotation logic is now duplicated for section and type toggles. Consider using a single shared rule:

.toggle-control.collapsed .toggle-icon { transform: rotate(0deg); }
.toggle-control:not(.collapsed) .toggle-icon { transform: rotate(90deg); }

and add toggle-control to both .section-toggle and .type-toggle to DRY the stylesheet.

tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs (1)

425-426: Reduce duplication: extract an exit-code assertion helper (optional)

The “0 or 1” assertion is repeated in many places. Consider a small helper to centralize this logic and message format.

Example helper within this test class:

private static void AssertExitCodeIsOk(int exitCode, string contextMessage)
{
    Assert.Contains(exitCode, new[] { 0, 1 });
}

Then replace usages like:

Assert.True(result.ExitCode == 0 || result.ExitCode == 1, $"CLI should succeed with namespace filtering. Exit code: {result.ExitCode}");

with:

AssertExitCodeIsOk(result.ExitCode, "CLI should succeed with namespace filtering");
tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs (2)

74-74: Emoji-based assertion may be brittle (optional)

Asserting on the “📁” glyph can be fragile across encodings/renderers. Consider adding a structural assertion too (e.g., for a group header element/class) to reduce coupling to the icon.


93-93: ElementName includes containing type — ensure output reflects it

Good update to align with grouping by containing type. Consider asserting the fully-qualified member name appears to strengthen the test.

         Assert.Contains("Removed Items", report);
         Assert.Contains("OldMethod", report);
         Assert.Contains("Removed method", report);
+        Assert.Contains("OldClass.OldMethod", report);
         Assert.Contains("📁 OldClass", report); // Type group header should show type name
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70a6209 and 6474f1b.

📒 Files selected for processing (10)
  • src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs (1 hunks)
  • src/DotNetApiDiff/Models/ApiComparison.cs (2 hunks)
  • src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs (6 hunks)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/change-group.scriban (1 hunks)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban (2 hunks)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js (2 hunks)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/styles.css (2 hunks)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/type-group.scriban (1 hunks)
  • tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs (10 hunks)
  • tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs (1)
tests/DotNetApiDiff.Tests/Integration/TypeMappingIntegrationTests.cs (1)
  • ExitCode (79-132)
🪛 GitHub Actions: Code Quality
src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs

[error] 137-137: dotnet format: Fix whitespace formatting. Replace 22 characters with '\n '.


[error] 138-138: dotnet format: Fix whitespace formatting. Replace 22 characters with '\n '.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build on windows-latest
🔇 Additional comments (11)
src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban (1)

155-170: ID slugging misses several unsafe characters

group.key is only stripped of space, dot and back-tick. Type names may include +, <, >, ,, etc. Those produce invalid IDs and break document.getElementById. Consider a dedicated slug helper that removes/escapes everything except [A-Za-z0-9_-].

tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs (9)

141-142: Exit code 1 for breaking changes — aligned and OK

Accepting 0 or 1 here matches the updated semantics. Looks good.


166-167: Consistent exit code expectation — OK

Assertion updated to 0 or 1 (was 0 or 2). Aligned with new behavior.


201-202: Consistent exit code expectation — OK

Matches the new “breaking changes => exit code 1” mapping.


236-237: Consistent exit code expectation — OK

Aligned with updated ExitCodeManager behavior.


367-368: Consistent exit code expectation — OK

The change reads correctly and aligns with the rest.


448-449: Consistent exit code expectation — OK

Keeps test intent clear with updated codes.


471-472: Consistent exit code expectation — OK

Aligned with new behavior and consistent with other tests.


617-618: Consistent exit code expectation — OK

Matches new semantics; assertion reads well.


539-540: No remaining references to exit code 2
I searched the entire test suite and codebase for any expectations or comments around exit code 2 and found none. Restricting the CLI to exit codes 0 or 1 is safe.

• tests/DotNetApiDiff.Tests/Integration/CliWorkflowTests.cs (lines 539–540)
csharp // Exit code 1 is expected when there are breaking changes Assert.True(result.ExitCode == 0 || result.ExitCode == 1, $"CLI should execute successfully. Exit code: {result.ExitCode}");

tests/DotNetApiDiff.Tests/Reporting/HtmlFormatterScribanTests.cs (1)

108-108: Type group header assertion — OK

Checks the expected group header after the grouping refactor. Looks good.

@jbrinkman jbrinkman merged commit 2d1312f into main Aug 8, 2025
7 of 8 checks passed
@jbrinkman jbrinkman deleted the feature/issue-51-api-comparison-validation-diagnostics branch August 8, 2025 19:23
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban (2)

102-107: Reintroduced nested interactive controls and non-keyboard-activatable header

Clickable div wrapping a button is an a11y anti-pattern; only the button should be interactive. Also add proper ARIA and type attributes.

Apply:

-            <div class="section-header" onclick="toggleSection('breaking-changes')">
+            <div class="section-header">
                 <h2>⚠️ Breaking Changes</h2>
-                <button class="section-toggle collapsed">
-                    <span class="toggle-icon">▶</span>
-                </button>
+                <button
+                    class="section-toggle collapsed"
+                    type="button"
+                    aria-controls="breaking-changes-content"
+                    aria-expanded="false"
+                    aria-label="Toggle breaking changes section"
+                    onclick="toggleSection('breaking-changes')">
+                    <span class="toggle-icon">▶</span>
+                </button>
             </div>

142-147: Same a11y issue in other change sections — make the button the only interactive control

Remove onclick from the header div; move it to the button and add ARIA.

-            <div class="section-header" onclick="toggleSection('{{ section.change_type }}-items')">
+            <div class="section-header">
                 <h2>{{ section.icon }} {{ section.title }} ({{ section.count }})</h2>
-                <button class="section-toggle collapsed">
-                    <span class="toggle-icon">▶</span>
-                </button>
+                <button
+                    class="section-toggle collapsed"
+                    type="button"
+                    aria-controls="{{ section.change_type }}-items-content"
+                    aria-expanded="false"
+                    aria-label="Toggle {{ section.title }} section"
+                    onclick="toggleSection('{{ section.change_type }}-items')">
+                    <span class="toggle-icon">▶</span>
+                </button>
             </div>
🧹 Nitpick comments (1)
src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban (1)

154-172: ID generation: strengthen slugging to avoid selector issues and collisions

The current replace chain handles spaces, dots, and backticks only. Type identifiers can include other characters (e.g., <T>, +, [], ,). Prefer a stable slug (or short hash) computed once in the view model and reused for:

  • data-type-group-id
  • the corresponding "-content" id
    This will reduce risk of invalid IDs and simplify template code.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6474f1b and 70375df.

📒 Files selected for processing (6)
  • src/DotNetApiDiff/Commands/CompareCommand.cs (1 hunks)
  • src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs (2 hunks)
  • src/DotNetApiDiff/Models/ApiComparison.cs (2 hunks)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/change-group.scriban (1 hunks)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban (2 hunks)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js (5 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/DotNetApiDiff/ExitCodes/ExitCodeManager.cs
  • src/DotNetApiDiff/Commands/CompareCommand.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/DotNetApiDiff/Reporting/HtmlTemplates/change-group.scriban
  • src/DotNetApiDiff/Models/ApiComparison.cs
  • src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js
🔇 Additional comments (1)
src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban (1)

178-185: Ensure HTML-escaping of dynamic content

User-controlled fields may include markup or malicious content. In
src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban around lines 178–185, wrap both change.element_name and change.description in an HTML-escape filter:

• At line ~178:

-   <code>{{ change.element_name }}</code>
+   <code>{{ change.element_name | html.escape }}</code>

• At line ~184:

-   <div class="change-description">{{ change.description }}</div>
+   <div class="change-description">{{ change.description | html.escape }}</div>

If Scriban doesn’t provide a built-in html.escape filter, please add one or pre-encode these values upstream before rendering.

Comment on lines +154 to +171
<div class="type-group" data-type-group-id="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}">
<div class="type-header">
<h3 class="type-name">
📁 {{ group.key }}
<span class="type-summary">
({{ group.count }} change{{ if group.count != 1 }}s{{ end }}{{ if group.has_breaking_changes }}, {{ group.breaking_changes_count }} breaking{{ end }})
</span>
</h3>
<div class="type-header-controls">
{{if group.has_breaking_changes}}
<span class="breaking-type-badge">⚠️ Breaking Changes</span>
{{end}}
<button class="type-toggle collapsed" onclick="toggleTypeGroup(this.closest('.type-group').dataset.typeGroupId)">
<span class="toggle-icon">▶</span>
</button>
</div>
</div>

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Type-group toggle button lacks ARIA and type; add attributes and keep state in sync

Add type, aria-controls, aria-expanded, and an accessible label. Ensure the toggle script updates aria-expanded accordingly.

-                            <button class="type-toggle collapsed" onclick="toggleTypeGroup(this.closest('.type-group').dataset.typeGroupId)">
+                            <button
+                                class="type-toggle collapsed"
+                                type="button"
+                                aria-controls="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}-content"
+                                aria-expanded="false"
+                                aria-label="Toggle type group: {{ group.key }}"
+                                onclick="toggleTypeGroup(this.closest('.type-group').dataset.typeGroupId)">
                                 <span class="toggle-icon">▶</span>
                             </button>

Additionally, consider computing a pre-slugged, safe group ID in the view model (or a helper) rather than ad-hoc replaces to avoid edge cases with characters like <, >, +, [, ], ,, : and to guarantee uniqueness.


Add ARIA attributes and explicit button type to the toggle control

To make the type-group toggle accessible and prevent unintended form submissions, the <button> needs an explicit type="button", aria-controls pointing at its content region, aria-expanded to reflect its state, and a descriptive aria-label. Ensure your toggleTypeGroup script updates aria-expanded when opening/closing.

Locations:

  • File: src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban
  • Around lines 154–171, update the <button class="type-toggle collapsed"> element.

Suggested diff:

-                            <button class="type-toggle collapsed" onclick="toggleTypeGroup(this.closest('.type-group').dataset.typeGroupId)">
+                            <button
+                                class="type-toggle collapsed"
+                                type="button"
+                                aria-controls="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}-content"
+                                aria-expanded="false"
+                                aria-label="Toggle type group: {{ group.key }}"
+                                onclick="toggleTypeGroup(this.closest('.type-group').dataset.typeGroupId)">
                                 <span class="toggle-icon">▶</span>
                             </button>

Additionally, consider generating a sanitized ID for each group in your view-model or via a helper (instead of ad-hoc string.replace chains) to guard against special characters (< > + [ ] , : etc.) and ensure global uniqueness.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="type-group" data-type-group-id="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}">
<div class="type-header">
<h3 class="type-name">
📁 {{ group.key }}
<span class="type-summary">
({{ group.count }} change{{ if group.count != 1 }}s{{ end }}{{ if group.has_breaking_changes }}, {{ group.breaking_changes_count }} breaking{{ end }})
</span>
</h3>
<div class="type-header-controls">
{{if group.has_breaking_changes}}
<span class="breaking-type-badge">⚠️ Breaking Changes</span>
{{end}}
<button class="type-toggle collapsed" onclick="toggleTypeGroup(this.closest('.type-group').dataset.typeGroupId)">
<span class="toggle-icon">▶</span>
</button>
</div>
</div>
<div class="type-header-controls">
{{if group.has_breaking_changes}}
<span class="breaking-type-badge">⚠️ Breaking Changes</span>
{{end}}
<button
class="type-toggle collapsed"
type="button"
aria-controls="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}-content"
aria-expanded="false"
aria-label="Toggle type group: {{ group.key }}"
onclick="toggleTypeGroup(this.closest('.type-group').dataset.typeGroupId)">
<span class="toggle-icon">▶</span>
</button>
</div>
🤖 Prompt for AI Agents
In src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban around lines
154 to 171, the toggle button lacks accessibility attributes and an explicit
button type. Add type="button" to prevent form submission, include aria-controls
referencing the ID of the collapsible content, aria-expanded reflecting the
toggle state, and a descriptive aria-label for screen readers. Modify the
toggleTypeGroup script to update aria-expanded on toggle. Also, replace the
current string.replace chain for generating IDs with a more robust sanitization
method or helper to ensure valid, unique IDs free of problematic characters.

Comment on lines +172 to +211
<div class="type-changes collapsed" id="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}-content">
<div class="category-changes">
{{for change in group.changes}}
<div class="change-item {{ section.change_type }}">
<div class="change-header">
<div class="change-name">
<span class="element-type">{{ change.element_type }}</span>
<code>{{ change.element_name }}</code>
{{if change.is_breaking_change}}
<span class="breaking-badge">BREAKING</span>
{{end}}
</div>
<div class="change-description">{{ change.description }}</div>
</div>
{{if change.has_signatures}}
<div class="signature-toggle">
<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
<span class="toggle-icon">▼</span> View Signature Details
</button>
</div>
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
{{if change.old_signature}}
<div class="signature-section">
<h5>Old Signature:</h5>
<pre><code class="csharp">{{ change.old_signature }}</code></pre>
</div>
{{end}}
{{if change.new_signature}}
<div class="signature-section">
<h5>New Signature:</h5>
<pre><code class="csharp">{{ change.new_signature }}</code></pre>
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Signature details toggle: missing ARIA on button; region should be associated; ensure signatures are HTML-escaped

  • Add type and ARIA to the toggle button; keep aria-expanded in sync in toggleSignature.
  • Ensure signature content is HTML-escaped to prevent markup injection within code blocks.
-                                    <button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
-                                        <span class="toggle-icon">▼</span> View Signature Details
-                                    </button>
+                                    <button
+                                        class="toggle-btn"
+                                        type="button"
+                                        aria-controls="{{ change.details_id }}"
+                                        aria-expanded="false"
+                                        aria-label="Toggle signature details for {{ change.element_name }}"
+                                        onclick="toggleSignature('{{ change.details_id }}')">
+                                        <span class="toggle-icon">▼</span> View Signature Details
+                                    </button>
-                                <div id="{{ change.details_id }}" class="signature-details" style="display: none;">
+                                <div id="{{ change.details_id }}" class="signature-details" style="display: none;">
-                                        <pre><code class="csharp">{{ change.old_signature }}</code></pre>
+                                        <pre><code class="csharp">{{ change.old_signature | html.escape }}</code></pre>
-                                        <pre><code class="csharp">{{ change.new_signature }}</code></pre>
+                                        <pre><code class="csharp">{{ change.new_signature | html.escape }}</code></pre>

If html.escape isn’t available in your Scriban setup, please pre-encode these values in the view model before rendering.


🏁 Script executed:

#!/bin/bash
# Search for the toggleSignature implementation to verify ARIA-expanded handling
rg "toggleSignature" -n .

# Check whether an html.escape filter (or equivalent) is already available in Scriban templates
rg "html\.escape" -n .
rg "html\.encode" -n .

Length of output: 695


🏁 Script executed:

#!/bin/bash
# Show the implementation of toggleSignature to verify ARIA-expanded toggling
sed -n '1,200p' src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js

Length of output: 6981


🌐 Web query:

Does Scriban provide a built-in html.escape or html.encode filter/function?

💡 Result:

Yes, Scriban provides a built-in html.escape function that escapes a string for safe inclusion in HTML. This function replaces special HTML characters with their corresponding HTML entities, ensuring that the string can be safely embedded within HTML content.

Usage:

{{ "<p>This is a paragraph</p>" | html.escape }}

Output:

&lt;p&gt;This is a paragraph&lt;/p&gt;

In this example, the < and > characters are replaced with &lt; and &gt;, respectively, to prevent them from being interpreted as HTML tags.

For more details on Scriban's built-in functions, you can refer to the official documentation. (github.com)

Citations:


Enhance signature-toggle accessibility and escape signature content

To meet ARIA best practices and prevent markup injection, please:

  • Update the toggle <button> in all signature‐details templates (main-layout.scriban, change-group.scriban, type-group.scriban):
    • Add type="button", aria-controls="{{ change.details_id }}", aria-expanded="false", and a descriptive aria-label.
  • Escape user‐provided signature text using Scriban’s built-in html.escape filter on both old_signature and new_signature.
  • In src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js, keep aria-expanded in sync when you expand or collapse:
    • Set button.setAttribute('aria-expanded', 'true') on expand and 'false' on collapse.

Example template diff:

--- a/.../main-layout.scriban
+++ b/.../main-layout.scriban
@@ -185,7 +185,14 @@
-<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
-  <span class="toggle-icon">▼</span> View Signature Details
-</button>
+<button
+  class="toggle-btn"
+  type="button"
+  aria-controls="{{ change.details_id }}"
+  aria-expanded="false"
+  aria-label="Toggle signature details for {{ change.element_name }}"
+  onclick="toggleSignature('{{ change.details_id }}')">
+  <span class="toggle-icon">▼</span> View Signature Details
+</button>
@@ -195,7 +202,7 @@
-<pre><code class="csharp">{{ change.old_signature }}</code></pre>
+<pre><code class="csharp">{{ change.old_signature | html.escape }}</code></pre>
@@ -200,7 +207,7 @@
-<pre><code class="csharp">{{ change.new_signature }}</code></pre>
+<pre><code class="csharp">{{ change.new_signature | html.escape }}</code></pre>

Example scripts.js diff:

--- a/src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js
+++ b/src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js
 function toggleSignature(detailsId) {
@@ -6,9 +6,13 @@
     if (details.style.display === 'none') {
         details.style.display = 'block';
         button.classList.add('expanded');
+        button.setAttribute('aria-expanded', 'true');
         button.innerHTML = '<span class="toggle-icon">▲</span> Hide Signature Details';
     } else {
         details.style.display = 'none';
         button.classList.remove('expanded');
+        button.setAttribute('aria-expanded', 'false');
         button.innerHTML = '<span class="toggle-icon">▼</span> View Signature Details';
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="type-changes collapsed" id="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}-content">
<div class="category-changes">
{{for change in group.changes}}
<div class="change-item {{ section.change_type }}">
<div class="change-header">
<div class="change-name">
<span class="element-type">{{ change.element_type }}</span>
<code>{{ change.element_name }}</code>
{{if change.is_breaking_change}}
<span class="breaking-badge">BREAKING</span>
{{end}}
</div>
<div class="change-description">{{ change.description }}</div>
</div>
{{if change.has_signatures}}
<div class="signature-toggle">
<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
<span class="toggle-icon">▼</span> View Signature Details
</button>
</div>
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
{{if change.old_signature}}
<div class="signature-section">
<h5>Old Signature:</h5>
<pre><code class="csharp">{{ change.old_signature }}</code></pre>
</div>
{{end}}
{{if change.new_signature}}
<div class="signature-section">
<h5>New Signature:</h5>
<pre><code class="csharp">{{ change.new_signature }}</code></pre>
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
<div class="type-changes collapsed" id="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}-content">
<div class="category-changes">
{{for change in group.changes}}
<div class="change-item {{ section.change_type }}">
<div class="change-header">
<div class="change-name">
<span class="element-type">{{ change.element_type }}</span>
<code>{{ change.element_name }}</code>
{{if change.is_breaking_change}}
<span class="breaking-badge">BREAKING</span>
{{end}}
</div>
<div class="change-description">{{ change.description }}</div>
</div>
{{if change.has_signatures}}
<div class="signature-toggle">
<button
class="toggle-btn"
type="button"
aria-controls="{{ change.details_id }}"
aria-expanded="false"
aria-label="Toggle signature details for {{ change.element_name }}"
onclick="toggleSignature('{{ change.details_id }}')">
<span class="toggle-icon">▼</span> View Signature Details
</button>
</div>
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
{{if change.old_signature}}
<div class="signature-section">
<h5>Old Signature:</h5>
<pre><code class="csharp">{{ change.old_signature | html.escape }}</code></pre>
</div>
{{end}}
{{if change.new_signature}}
<div class="signature-section">
<h5>New Signature:</h5>
<pre><code class="csharp">{{ change.new_signature | html.escape }}</code></pre>
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
🤖 Prompt for AI Agents
In src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban lines 172 to
211, improve accessibility by adding type="button", aria-controls referencing
the signature details ID, aria-expanded="false", and a descriptive aria-label to
the toggle button for signature details. Also, apply the html.escape filter to
both old_signature and new_signature to prevent markup injection. Additionally,
in src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js, update the
toggleSignature function to set the button's aria-expanded attribute to "true"
when expanding and "false" when collapsing the signature details, keeping ARIA
state in sync with visibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhancement: Add detailed validation error messages to ApiComparison

2 participants