Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a 🚥 Pre-merge checks | ✅ 1 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/providers/diagnostics/index.ts (2)
61-83: Async callbacks inforEachdon't await – diagnostics may arrive out of order or be incomplete on rapid edits.Using
forEachwith async callbacks fires all promises concurrently without awaiting them. Combined with the nested asyncforEachfor rules, this means:
collectDiagnosticsreturns before any diagnostics are actually collected- If the document changes rapidly, a new
collectDiagnosticscall could clear diagnostics while the previous batch is still being fetchedThe debounced
flushmitigates some UI flicker, but consider usingPromise.allwithmapfor more predictable behaviour.♻️ Suggested refactor using Promise.all
- dependencies.forEach(async (dep) => { + await Promise.all(dependencies.map(async (dep) => { try { const pkg = await getPackageInfo(dep.name) if (!pkg) return - enabledRules.value.forEach(async (rule) => { + await Promise.all(enabledRules.value.map(async (rule) => { const diagnostic = await rule(dep, pkg) if (diagnostic) { diagnostics.push({ source: displayName, range: extractor.getNodeRange(document, diagnostic.node), ...diagnostic, }) flush() } - }) + })) } catch (err) { logger.warn(`Failed to check ${dep.name}: ${err}`) } - }) + }))
48-48: Consider: clearing diagnostics immediately may cause visual flicker.Deleting diagnostics at the start of
collectDiagnosticsmeans users see a brief flash where all diagnostics disappear before new ones appear. An alternative is to build the new diagnostics set first and then replace atomically, though this adds complexity.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/providers/diagnostics/index.ts (2)
64-86:⚠️ Potential issue | 🟠 Major
forEachwith async callbacks does not await—diagnostics collection is fire-and-forget.Using
forEachwith async callbacks means the iterations are not awaited. Eachdepandrulecallback fires independently, andcollectDiagnosticsreturns before any diagnostics are actually collected. This can cause race conditions when the document text changes rapidly.Consider using
Promise.allwithmapor afor...ofloop:🐛 Proposed fix using Promise.all
- dependencies.forEach(async (dep) => { - try { - const pkg = await getPackageInfo(dep.name) - if (!pkg) - return - - enabledRules.value.forEach(async (rule) => { - const diagnostic = await rule(dep, pkg) - - if (diagnostic) { - diagnostics.push({ - source: displayName, - range: extractor.getNodeRange(document, diagnostic.node), - ...diagnostic, - }) - - flush() - } - }) - } catch (err) { - logger.warn(`Failed to check ${dep.name}: ${err}`) - } - }) + await Promise.all(dependencies.map(async (dep) => { + try { + const pkg = await getPackageInfo(dep.name) + if (!pkg) + return + + const results = await Promise.all(enabledRules.value.map(rule => rule(dep, pkg))) + for (const diagnostic of results) { + if (diagnostic) { + diagnostics.push({ + source: displayName, + range: extractor.getNodeRange(document, diagnostic.node), + ...diagnostic, + }) + } + } + flush() + } catch (err) { + logger.warn(`Failed to check ${dep.name}: ${err}`) + } + }))
60-62:⚠️ Potential issue | 🟠 MajorDebounced function created inside
collectDiagnosticsdefeats the debouncing purpose.Each invocation of
collectDiagnosticscreates a new debouncedflushfunction with its own timer. When the watcher triggers rapid successive calls, each call gets an independent debounce, so they don't consolidate as intended.Move
flushoutsidecollectDiagnosticsor use a module-level debounced function with the document URI as a key.🛠️ Suggested approach
+ const flush = debounce((uri: Uri, items: Diagnostic[]) => { + diagnosticCollection.set(uri, items) + }, 100) + async function collectDiagnostics() { // ... const diagnostics: Diagnostic[] = [] - - const flush = debounce(() => { - diagnosticCollection.set(document.uri, [...diagnostics]) - }, 100) // Inside the loop, call: - flush() + flush(document.uri, [...diagnostics]) }
🧹 Nitpick comments (1)
src/providers/diagnostics/index.ts (1)
89-89: Consider adding cancellation for in-flight diagnostic collections.When
activeDocumentTextchanges rapidly (e.g., during typing), multiplecollectDiagnosticscalls may overlap. Without cancellation, older collections continue running and may overwrite results from newer collections.Consider using an
AbortControlleror a version counter to discard stale results.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/providers/diagnostics/index.ts (1)
64-86:⚠️ Potential issue | 🟠 MajorAsync
forEachdoes not await callbacks, causing race conditions.The nested
forEach(async ...)pattern is fire-and-forget. The outercollectDiagnosticsfunction returns immediately whilst async operations continue in the background. If the function is called again before prior operations complete (e.g., rapid document edits), multiple invocations will race and potentially intermingle diagnostics from different document states.Consider using
for...ofwithawaitfor sequential processing, orPromise.allwithmapfor parallel processing with proper completion tracking.🛠️ Proposed fix using Promise.all for parallel processing
- dependencies.forEach(async (dep) => { - try { - const pkg = await getPackageInfo(dep.name) - if (!pkg) - return - - enabledRules.value.forEach(async (rule) => { - const diagnostic = await rule(dep, pkg) - - if (diagnostic) { - diagnostics.push({ - source: displayName, - range: extractor.getNodeRange(document, diagnostic.node), - ...diagnostic, - }) - - flush() - } - }) - } catch (err) { - logger.warn(`Failed to check ${dep.name}: ${err}`) - } - }) + await Promise.all(dependencies.map(async (dep) => { + try { + const pkg = await getPackageInfo(dep.name) + if (!pkg) + return + + const results = await Promise.all(enabledRules.value.map(rule => rule(dep, pkg))) + + for (const diagnostic of results) { + if (diagnostic) { + diagnostics.push({ + source: displayName, + range: extractor.getNodeRange(document, diagnostic.node), + ...diagnostic, + }) + } + } + flush() + } + catch (err) { + logger.warn(`Failed to check ${dep.name}: ${err}`) + } + }))
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/providers/diagnostics/index.ts (1)
64-86:⚠️ Potential issue | 🟠 MajorRace condition:
forEachwith async callbacks does not await, causing concurrent mutations.
Array.forEach()ignores the returned promises, so all dependency checks run concurrently without coordination. The shareddiagnosticsarray is mutated by multiple concurrent async callbacks, leading to:
- Unpredictable ordering of diagnostics
- If the document changes mid-collection, stale async work continues pushing to the old array
collectDiagnosticsreturns before work completes, so rapid document edits can interleave results🛠️ Proposed fix using Promise.all with proper async/await
- dependencies.forEach(async (dep) => { + await Promise.all(dependencies.map(async (dep) => { try { const pkg = await getPackageInfo(dep.name) if (!pkg) return - enabledRules.value.forEach(async (rule) => { - const diagnostic = await rule(dep, pkg) - - if (diagnostic) { - diagnostics.push({ - source: displayName, - range: extractor.getNodeRange(document, diagnostic.node), - ...diagnostic, - }) - - flush(document.uri, diagnostics) - } - }) + const results = await Promise.all( + enabledRules.value.map(rule => rule(dep, pkg)) + ) + + for (const diagnostic of results) { + if (diagnostic) { + diagnostics.push({ + source: displayName, + range: extractor.getNodeRange(document, diagnostic.node), + ...diagnostic, + }) + } + } } catch (err) { logger.warn(`Failed to check ${dep.name}: ${err}`) } - }) + })) + + flush(document.uri, diagnostics)This also moves
flushoutside the loop, calling it once after all diagnostics are collected, which is more efficient and avoids redundant debounced calls.
This PR aims to make logic more reactive