Skip to content

feat: replace AJV with vscode-json-languageservice - #474

Merged
dmitriy-kostianetskiy merged 15 commits into
mainfrom
feature/vscode-json-validation
Mar 17, 2026
Merged

dmitriy-kostianetskiy merged 15 commits into
mainfrom
feature/vscode-json-validation

Conversation

@dmitriy-kostianetskiy

@dmitriy-kostianetskiy dmitriy-kostianetskiy commented Mar 17, 2026 •

Copy link
Copy Markdown
Collaborator

This pull request significantly refactors JSON schema validation in the codemirror package, replacing the previous ajv-based implementation with the vscode-json-languageservice for more accurate, IDE-quality diagnostics and improved tooltip rendering. It also introduces a new diagnostic tooltip component, updates tooltip and theme styling, and improves test coverage for schema integration. Additionally, it standardizes the React-based rendering utility used for tooltips and documentation across the codebase.

JSON Schema Validation Refactor:

  • Replaced ajv and jsonc-parser with vscode-json-languageservice for JSON schema validation, providing more precise and actionable diagnostics and leveraging VS Code’s language tooling. (packages/codemirror/src/utils/create-json-schema-validator.ts, packages/codemirror/src/utils/validate-json-schema.ts, packages/codemirror/package.json) [1] [2] [3]
  • Updated the linter extension to use the new async validator and simplified its API. (packages/codemirror/src/extensions/json-schema-lint.ts)

Tooltip and Diagnostic UI Improvements:

  • Added a new DiagnosticTooltip React component for consistent, styled diagnostic tooltips. (packages/codemirror/src/components/DiagnosticTooltip.tsx)
  • Enhanced tooltip theming: improved font, spacing, and border styles for diagnostics and tooltips, and ensured accessibility across overflow containers. (packages/codemirror/src/themes/tooltip-theme.ts, packages/codemirror/src/themes/base-theme.ts) [1] [2]

Testing and Mocking:

  • Added a comprehensive integration test for Vega-Lite schema validation to ensure correct diagnostic behavior. (packages/codemirror/__tests__/vega-schema-integration.test.ts)
  • Provided a mock implementation for the React rendering utility used in tests. (packages/codemirror/__mocks__/@sqlrooms/utils.ts)

Code Consistency and Cleanup:

  • Standardized usage of renderComponentToDomElement for rendering React components to DOM elements in tooltips and documentation, replacing previous usage of string-based rendering. (packages/sql-editor/src/codemirror/extensions/completion.ts, packages/sql-editor/src/codemirror/extensions/hover.ts) [1] [2] [3] [4]
  • Removed unused imports and code related to the previous lint gutter and validation logic. (packages/codemirror/src/components/JsonCodeMirrorEditor.tsx) [1] [2]

These changes modernize and unify schema validation, diagnostics, and tooltip rendering, resulting in a better developer and user experience.

image

Summary by CodeRabbit

  • New Features

    • Improved JSON validation with enhanced diagnostic tooltips showing message formatting and error codes
  • Bug Fixes

    • Fixed unexpected popover closures when interacting with editor diagnostic tooltips
  • Improvements

    • Upgraded underlying JSON validation system for comprehensive schema validation and better error detection
    • Refined visual design of diagnostic messages with monospace fonts and improved spacing for clarity

@dmitriy-kostianetskiy dmitriy-kostianetskiy self-assigned this Mar 17, 2026
@coderabbitai

coderabbitai Bot commented Mar 17, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR refactors JSON schema validation from ajv to vscode-json-languageservice, consolidates React rendering utilities into a single module with extended functionality, updates consumers to use the new rendering API, enhances diagnostic tooltip styling, and expands test coverage including Vega-Lite schema integration.

Changes

Cohort / File(s) Summary
Configuration
.gitignore
Added .worktrees/ ignore entry and adjusted whitespace handling for .cursor/plans/.
Test Infrastructure & Mocks
packages/codemirror/__mocks__/@sqlrooms/utils.ts, packages/codemirror/__tests__/validate-json-schema.test.ts, packages/codemirror/__tests__/vega-schema-integration.test.ts
Added mock implementations for rendering utilities; converted validate-json-schema tests to async with relaxed assertions and expanded oneOf-schema coverage; added new Vega-Lite schema integration tests with real schema validation.
Validation Library Migration
packages/codemirror/src/utils/validate-json-schema.ts, packages/codemirror/src/utils/create-json-schema-validator.ts, packages/codemirror/src/extensions/json-schema-lint.ts, packages/codemirror/package.json
Replaced ajv/ajv-formats/jsonc-parser with vscode-json-languageservice; updated validateJsonSchema to async using LanguageService; removed maxErrors parameter and tooltipFilter; adjusted linter callback to async; updated package dependencies.
React Components & Utilities
packages/codemirror/src/components/DiagnosticTooltip.tsx, packages/utils/src/render-component.tsx, packages/utils/src/index.ts, packages/utils/src/renderComponentToString.tsx
Added DiagnosticTooltip component; consolidated rendering utilities from separate file into unified render-component module exporting both renderComponentToDomElement and renderComponentToString; removed old renderComponentToString file; updated index.ts exports.
Editor Integration & Consumer Updates
packages/sql-editor/src/codemirror/extensions/completion.ts, packages/sql-editor/src/codemirror/extensions/hover.ts
Updated imports to use renderComponentToDomElement; changed DOM handling to leverage returned { dom, destroy } object for tooltip creation and lifecycle management.
Styling & UI
packages/codemirror/src/themes/base-theme.ts, packages/codemirror/src/themes/tooltip-theme.ts, packages/vega/src/VegaEditAction.tsx
Removed left border from diagnostic warnings; enhanced tooltip styling with monospace font, adjusted padding/line-height, improved diagnostic text rendering; wrapped Popover with modal={false} and added onInteractOutside handler to prevent closing on CodeMirror tooltip interaction.
CodeMirror Editor
packages/codemirror/src/components/JsonCodeMirrorEditor.tsx
Removed lintGutter import and integration from extensions array for schema-enabled cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat: code mirror editors #418: Directly overlaps with validator refactoring, rendering utility consolidation, and updates to codemirror/JSON-schema validation APIs and integration points across both codemirror and sql-editor packages.

Poem

🐰 From ajv's strict rules we hop away,
To vscode's schemas, bright and gay,
Render components with grace and ease,
Tooltips polished to please and appease,
One validation spring—let validation bloom! 🌱✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: replace AJV with vscode-json-languageservice' accurately summarizes the main change - replacing the AJV validation library with vscode-json-languageservice throughout the codemirror package.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/vscode-json-validation
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
packages/codemirror/__mocks__/@sqlrooms/utils.ts (1)

1-2: Update the file header comment for accuracy.

Line 2 says only the React rendering function is mocked, but this file also mocks getMonospaceFont.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/codemirror/__mocks__/`@sqlrooms/utils.ts around lines 1 - 2, Update
the file header comment to accurately reflect what is mocked: mention that both
the React rendering function and getMonospaceFont are mocked (not just the React
rendering function). Reference the mock file
packages/codemirror/__mocks__/@sqlrooms/utils.ts and the exported symbol
getMonospaceFont (and the React render function) so the header clearly states
both exports are mocked and the rest are no-ops.
packages/codemirror/src/utils/validate-json-schema.ts (2)

92-106: Use DiagnosticSeverity enum values instead of magic numbers.

The convertSeverity function uses literal numbers (1, 2, 3, 4) instead of the imported DiagnosticSeverity enum, reducing readability and maintainability.

♻️ Proposed fix to use enum values
 function convertSeverity(
   vsSeverity: DiagnosticSeverity | undefined,
 ): Diagnostic['severity'] {
   switch (vsSeverity) {
-    case 1:
+    case DiagnosticSeverity.Error:
       return 'error';
-    case 2:
+    case DiagnosticSeverity.Warning:
       return 'warning';
-    case 3:
+    case DiagnosticSeverity.Information:
       return 'info';
-    case 4:
+    case DiagnosticSeverity.Hint:
       return 'hint';
     default:
       return 'warning';
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/codemirror/src/utils/validate-json-schema.ts` around lines 92 - 106,
The convertSeverity function currently compares vsSeverity to magic numbers
(1-4); replace those numeric literals with the corresponding DiagnosticSeverity
enum members (DiagnosticSeverity.Error, DiagnosticSeverity.Warning,
DiagnosticSeverity.Information, DiagnosticSeverity.Hint) so the switch checks
enum values and returns the same string mapping ('error', 'warning', 'info',
'hint'), and keep the default return as 'warning'; update the switch cases in
convertSeverity to use the DiagnosticSeverity enum identifiers to improve
readability and maintainability.

57-62: React root leak in renderMessage callbacks.

Each diagnostic's renderMessage callback creates a new React root via renderComponentToDomElement. These roots are never unmounted when CodeMirror removes the diagnostic tooltips. With many diagnostics (e.g., a malformed JSON file), this could accumulate orphaned roots.

This is consistent with the pattern used elsewhere in this PR, but worth noting for awareness.

Also applies to: 76-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/codemirror/src/utils/validate-json-schema.ts` around lines 57 - 62,
renderMessage currently creates a new React root for each diagnostic by calling
renderComponentToDomElement(DiagnosticTooltip, ...) and never unmounts it,
leaking roots; change renderMessage to create the React root into the tooltip
DOM element and return a cleanup/unmount callback (or reuse a single root) so
the React tree is unmounted when CodeMirror disposes the tooltip; update both
occurrences (renderMessage at lines shown and the similar block around 76-80) to
call the appropriate unmount method (or keep a shared root) for the
DiagnosticTooltip instead of leaving roots orphaned.
packages/codemirror/src/utils/create-json-schema-validator.ts (1)

11-22: Consider adding a timeout for external schema fetches.

The schemaRequestService uses fetch without a timeout or AbortSignal. If an external schema URL is slow or unresponsive, validation could hang indefinitely.

⏱️ Proposed fix to add fetch timeout
     schemaRequestService: async (uri: string) => {
       try {
-        const response = await fetch(uri);
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
+        try {
+          const response = await fetch(uri, { signal: controller.signal });
+          clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/codemirror/src/utils/create-json-schema-validator.ts` around lines
11 - 22, The schemaRequestService implementation uses fetch without a timeout,
so slow/unresponsive schema URIs can hang validation; modify the
schemaRequestService async function to create an AbortController, start a timer
(e.g., timeoutMs constant or parameter) that calls controller.abort() after the
timeout, pass controller.signal to fetch(uri, { signal: controller.signal }),
clear the timer on success, and convert AbortError to a clear error message
before throwing; update references to schemaRequestService in
create-json-schema-validator to ensure the timeout is applied for all external
schema fetches.
packages/codemirror/__tests__/vega-schema-integration.test.ts (1)

7-13: Network-dependent test may be flaky in CI.

The test fetches the Vega-Lite schema from an external URL on every run. This introduces network dependency that could cause intermittent test failures in restricted environments or during GitHub outages.

Consider caching the schema locally or mocking the fetch in CI, while keeping the real fetch for local development verification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/codemirror/__tests__/vega-schema-integration.test.ts` around lines 7
- 13, The beforeAll block currently fetches the Vega-Lite schema from the
network via fetch into the schema variable, which makes the test flaky in CI;
change the test to use a deterministic local fixture or a mocked fetch: add a
committed JSON fixture of v5.json and in the test file load that fixture (or
conditionally replace global fetch with a jest mock that returns the fixture
when process.env.CI is set), keeping the existing fetch path for local
development if desired; update the beforeAll to read the local file or use the
mocked response for fetch so tests no longer depend on external network calls.
packages/sql-editor/src/codemirror/extensions/completion.ts (1)

52-60: Potential React root memory leak with tooltip DOM elements.

Each completion item creates a React root via renderComponentToDomElement that is never unmounted when CodeMirror discards the tooltip. While the utility documents this as intentional, frequent autocomplete usage could accumulate orphaned React roots.

Consider whether the completion frequency warrants a cleanup mechanism, or whether a simpler non-React approach (plain DOM or HTML string) might be more appropriate for transient tooltips.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sql-editor/src/codemirror/extensions/completion.ts` around lines 52
- 60, The tooltip DOM returned by info() currently uses
renderComponentToDomElement to mount a React root for FunctionDocumentation (via
renderComponentToDomElement) but never unmounts it, leaking roots when
CodeMirror discards tooltips; modify the completion creation so either (A) you
avoid mounting a persistent React root by returning a plain DOM/HTML string from
info() instead of renderComponentToDomElement, or (B) keep a reference to the
mounted root/node and ensure you call the React unmount API (e.g.,
root.unmount() or ReactDOM.unmountComponentAtNode) when CodeMirror removes the
tooltip (hook into the tooltip/tooltipClose or completion UI lifecycle),
updating the code paths around renderComponentToDomElement and the info() return
to perform this cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/codemirror/__tests__/validate-json-schema.test.ts`:
- Around line 333-334: The test contains a duplicate assertion checking messages
for "Missing property"; update the second assertion to verify the other missing
property names instead of repeating the same check: locate the assertions using
the messages array (expect(messages.some((m) => m.includes('Missing
property')))...), and change them to assert that messages include "Missing
property 'street'" and "Missing property 'country'" (or similarly targeted
substrings) so both missing properties are individually verified.

In `@packages/codemirror/src/components/DiagnosticTooltip.tsx`:
- Around line 12-14: The conditional rendering for the diagnostic code in
DiagnosticTooltip.tsx uses a truthy check ({code && ...}) which hides valid
numeric code 0; update the guard to explicitly check for non-null/undefined
(e.g., code !== null && code !== undefined) so a code value of 0 is rendered
correctly, keeping the span markup and className unchanged and ensuring the
component's declared type (string | number | null) is respected.

In `@packages/utils/src/render-component.tsx`:
- Around line 27-33: renderComponentToDomElement currently returns only the
container DOM node causing React roots to be lost; change
renderComponentToDomElement (and the underlying renderToContainer usage) to
return an object with both the container DOM element and a destroy() callback
that unmounts the React root (i.e., call the appropriate root.unmount() /
cleanup created when mounting), then update its call sites (hover.ts create =>
({dom, destroy}), completion.ts info => ({dom, destroy}),
validate-json-schema.ts renderMessage => ({dom, destroy})) to use the returned
destroy for CodeMirror TooltipView/Completion.info cleanup.

---

Nitpick comments:
In `@packages/codemirror/__mocks__/`@sqlrooms/utils.ts:
- Around line 1-2: Update the file header comment to accurately reflect what is
mocked: mention that both the React rendering function and getMonospaceFont are
mocked (not just the React rendering function). Reference the mock file
packages/codemirror/__mocks__/@sqlrooms/utils.ts and the exported symbol
getMonospaceFont (and the React render function) so the header clearly states
both exports are mocked and the rest are no-ops.

In `@packages/codemirror/__tests__/vega-schema-integration.test.ts`:
- Around line 7-13: The beforeAll block currently fetches the Vega-Lite schema
from the network via fetch into the schema variable, which makes the test flaky
in CI; change the test to use a deterministic local fixture or a mocked fetch:
add a committed JSON fixture of v5.json and in the test file load that fixture
(or conditionally replace global fetch with a jest mock that returns the fixture
when process.env.CI is set), keeping the existing fetch path for local
development if desired; update the beforeAll to read the local file or use the
mocked response for fetch so tests no longer depend on external network calls.

In `@packages/codemirror/src/utils/create-json-schema-validator.ts`:
- Around line 11-22: The schemaRequestService implementation uses fetch without
a timeout, so slow/unresponsive schema URIs can hang validation; modify the
schemaRequestService async function to create an AbortController, start a timer
(e.g., timeoutMs constant or parameter) that calls controller.abort() after the
timeout, pass controller.signal to fetch(uri, { signal: controller.signal }),
clear the timer on success, and convert AbortError to a clear error message
before throwing; update references to schemaRequestService in
create-json-schema-validator to ensure the timeout is applied for all external
schema fetches.

In `@packages/codemirror/src/utils/validate-json-schema.ts`:
- Around line 92-106: The convertSeverity function currently compares vsSeverity
to magic numbers (1-4); replace those numeric literals with the corresponding
DiagnosticSeverity enum members (DiagnosticSeverity.Error,
DiagnosticSeverity.Warning, DiagnosticSeverity.Information,
DiagnosticSeverity.Hint) so the switch checks enum values and returns the same
string mapping ('error', 'warning', 'info', 'hint'), and keep the default return
as 'warning'; update the switch cases in convertSeverity to use the
DiagnosticSeverity enum identifiers to improve readability and maintainability.
- Around line 57-62: renderMessage currently creates a new React root for each
diagnostic by calling renderComponentToDomElement(DiagnosticTooltip, ...) and
never unmounts it, leaking roots; change renderMessage to create the React root
into the tooltip DOM element and return a cleanup/unmount callback (or reuse a
single root) so the React tree is unmounted when CodeMirror disposes the
tooltip; update both occurrences (renderMessage at lines shown and the similar
block around 76-80) to call the appropriate unmount method (or keep a shared
root) for the DiagnosticTooltip instead of leaving roots orphaned.

In `@packages/sql-editor/src/codemirror/extensions/completion.ts`:
- Around line 52-60: The tooltip DOM returned by info() currently uses
renderComponentToDomElement to mount a React root for FunctionDocumentation (via
renderComponentToDomElement) but never unmounts it, leaking roots when
CodeMirror discards tooltips; modify the completion creation so either (A) you
avoid mounting a persistent React root by returning a plain DOM/HTML string from
info() instead of renderComponentToDomElement, or (B) keep a reference to the
mounted root/node and ensure you call the React unmount API (e.g.,
root.unmount() or ReactDOM.unmountComponentAtNode) when CodeMirror removes the
tooltip (hook into the tooltip/tooltipClose or completion UI lifecycle),
updating the code paths around renderComponentToDomElement and the info() return
to perform this cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 395f3f11-edc2-4cbd-a95f-a2c1005cd7e9

📥 Commits

Reviewing files that changed from the base of the PR and between 352fc65 and a715412.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • .gitignore
  • packages/codemirror/__mocks__/@sqlrooms/utils.ts
  • packages/codemirror/__tests__/validate-json-schema.test.ts
  • packages/codemirror/__tests__/vega-schema-integration.test.ts
  • packages/codemirror/package.json
  • packages/codemirror/src/components/DiagnosticTooltip.tsx
  • packages/codemirror/src/components/JsonCodeMirrorEditor.tsx
  • packages/codemirror/src/extensions/json-schema-lint.ts
  • packages/codemirror/src/themes/base-theme.ts
  • packages/codemirror/src/themes/tooltip-theme.ts
  • packages/codemirror/src/utils/create-json-schema-validator.ts
  • packages/codemirror/src/utils/validate-json-schema.ts
  • packages/sql-editor/src/codemirror/extensions/completion.ts
  • packages/sql-editor/src/codemirror/extensions/hover.ts
  • packages/utils/src/index.ts
  • packages/utils/src/render-component.tsx
  • packages/utils/src/renderComponentToString.tsx
  • packages/vega/src/VegaEditAction.tsx
💤 Files with no reviewable changes (1)
  • packages/utils/src/renderComponentToString.tsx

Comment thread packages/codemirror/__tests__/validate-json-schema.test.ts Outdated
Comment thread packages/codemirror/src/components/DiagnosticTooltip.tsx
Comment thread packages/utils/src/render-component.tsx
@dmitriy-kostianetskiy
dmitriy-kostianetskiy marked this pull request as ready for review March 17, 2026 11:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/codemirror/src/utils/validate-json-schema.ts (1)

92-106: Use DiagnosticSeverity enum constants for clarity.

Magic numbers reduce readability. The DiagnosticSeverity enum is already imported.

♻️ Proposed refactor
 function convertSeverity(
   vsSeverity: DiagnosticSeverity | undefined,
 ): Diagnostic['severity'] {
   switch (vsSeverity) {
-    case 1:
+    case DiagnosticSeverity.Error:
       return 'error';
-    case 2:
+    case DiagnosticSeverity.Warning:
       return 'warning';
-    case 3:
+    case DiagnosticSeverity.Information:
       return 'info';
-    case 4:
+    case DiagnosticSeverity.Hint:
       return 'hint';
     default:
       return 'warning';
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/codemirror/src/utils/validate-json-schema.ts` around lines 92 - 106,
The switch in convertSeverity uses numeric magic values; replace the case labels
1,2,3,4 with the corresponding DiagnosticSeverity enum constants (e.g.,
DiagnosticSeverity.Error, DiagnosticSeverity.Warning,
DiagnosticSeverity.Information, DiagnosticSeverity.Hint) so the mapping to
Diagnostic['severity'] is clear and maintain the existing default return
'warning'; update only the case labels in convertSeverity to use these enum
members.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/codemirror/src/utils/validate-json-schema.ts`:
- Around line 54-61: The Diagnostic.renderMessage implementation creates a React
root via renderComponentToDomElement (inside renderMessage) but never calls the
returned destroy/unmount, leaking roots; replace this by using
renderComponentToString to render DiagnosticTooltip to HTML and set that HTML
into a detached container (e.g., createElement('div').innerHTML =
renderComponentToString(DiagnosticTooltip, {message, code})) and return that
container DOM node from renderMessage (or alternatively track and call the
destroy function when diagnostics change); update the code paths that call
renderComponentToDomElement in renderMessage to use renderComponentToString and
avoid creating persistent React roots.

---

Nitpick comments:
In `@packages/codemirror/src/utils/validate-json-schema.ts`:
- Around line 92-106: The switch in convertSeverity uses numeric magic values;
replace the case labels 1,2,3,4 with the corresponding DiagnosticSeverity enum
constants (e.g., DiagnosticSeverity.Error, DiagnosticSeverity.Warning,
DiagnosticSeverity.Information, DiagnosticSeverity.Hint) so the mapping to
Diagnostic['severity'] is clear and maintain the existing default return
'warning'; update only the case labels in convertSeverity to use these enum
members.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e713276-efaf-4657-91a0-c153b8a13be0

📥 Commits

Reviewing files that changed from the base of the PR and between a715412 and 3b24ba0.

📒 Files selected for processing (7)
  • packages/codemirror/__mocks__/@sqlrooms/utils.ts
  • packages/codemirror/__tests__/validate-json-schema.test.ts
  • packages/codemirror/__tests__/vega-schema-integration.test.ts
  • packages/codemirror/src/utils/validate-json-schema.ts
  • packages/sql-editor/src/codemirror/extensions/completion.ts
  • packages/sql-editor/src/codemirror/extensions/hover.ts
  • packages/utils/src/render-component.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/codemirror/tests/vega-schema-integration.test.ts
  • packages/sql-editor/src/codemirror/extensions/completion.ts

Comment on lines +54 to +61
renderMessage: () => {
const {dom} = renderComponentToDomElement(DiagnosticTooltip, {
message,
code,
});

return dom;
},

@coderabbitai coderabbitai Bot Mar 17, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Memory leak: React root not cleaned up in renderMessage.

The destroy callback is destructured but discarded. Unlike TooltipView and Completion.info, CodeMirror's Diagnostic.renderMessage returns only a DOM node and doesn't provide a lifecycle hook for cleanup. Each time a diagnostic tooltip renders, a new React root is created but never unmounted.

Consider either:

  1. Using renderComponentToString and setting innerHTML (simpler, no React root lifecycle)
  2. Tracking roots externally and cleaning up when diagnostics change
💡 Option 1: Use renderComponentToString instead
       renderMessage: () => {
-        const {dom} = renderComponentToDomElement(DiagnosticTooltip, {
+        const html = renderComponentToString(DiagnosticTooltip, {
           message,
           code,
         });
-
-        return dom;
+        const dom = document.createElement('div');
+        dom.innerHTML = html;
+        return dom;
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/codemirror/src/utils/validate-json-schema.ts` around lines 54 - 61,
The Diagnostic.renderMessage implementation creates a React root via
renderComponentToDomElement (inside renderMessage) but never calls the returned
destroy/unmount, leaking roots; replace this by using renderComponentToString to
render DiagnosticTooltip to HTML and set that HTML into a detached container
(e.g., createElement('div').innerHTML =
renderComponentToString(DiagnosticTooltip, {message, code})) and return that
container DOM node from renderMessage (or alternatively track and call the
destroy function when diagnostics change); update the code paths that call
renderComponentToDomElement in renderMessage to use renderComponentToString and
avoid creating persistent React roots.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks like renderComponentToString could be a simple solution?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@ilyabo Yes, renderComponentToString is the right fix here. It renders the component to an HTML string and immediately unmounts the React root before returning, so there's no persistent root to leak. Since DiagnosticTooltip is a purely presentational component, string rendering is perfectly sufficient.

Here's the concrete change for both renderMessage call sites in validate-json-schema.ts:

- import {renderComponentToDomElement} from '@sqlrooms/utils';
+ import {renderComponentToString} from '@sqlrooms/utils';

 // Inside the vsDiagnostics loop:
       renderMessage: () => {
-        const {dom} = renderComponentToDomElement(DiagnosticTooltip, {
+        const html = renderComponentToString(DiagnosticTooltip, {
           message,
           code,
         });
-        return dom;
+        const dom = document.createElement('div');
+        dom.innerHTML = html;
+        return dom;
       },

 // Inside the catch block:
       renderMessage: () => {
-        const {dom} = renderComponentToDomElement(DiagnosticTooltip, {
+        const html = renderComponentToString(DiagnosticTooltip, {
           message,
         });
-        return dom;
+        const dom = document.createElement('div');
+        dom.innerHTML = html;
+        return dom;
       },

This also aligns well with how renderComponentToString is already used elsewhere in the codebase (e.g., the old hover/completion tooltips before this PR refactored them to use renderComponentToDomElement — but those have a destroy callback path, which renderMessage lacks).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The renderMessage API requires a Node, not a string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@dmitriy-kostianetskiy
dmitriy-kostianetskiy merged commit 0d16adb into main Mar 17, 2026
4 checks passed
@dmitriy-kostianetskiy
dmitriy-kostianetskiy deleted the feature/vscode-json-validation branch March 17, 2026 13:08
dmitriy-kostianetskiy added a commit that referenced this pull request Mar 17, 2026
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.

2 participants