Skip to content
Merged
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
223 changes: 223 additions & 0 deletions docs/analysis-error-investigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Investigation: Extension Throws Error During Analysis

## Summary

This document captures all potential root causes identified in the source code
that can lead to a visible error (or a silent failure) when the user triggers
any of the **Explain** commands (`Explain Line`, `Explain Block`,
`Explain File`).

---

## 1. `ExplanationProvider.explain` β€” unguarded `LanguageModelError` codes
**File:** `src/explanationProvider.ts`

```ts
} catch (err) {
if (err instanceof vscode.LanguageModelError) {
throw new Error(`Copilot error (${err.code}): ${err.message}`);
}
throw err;
}
```

### Issue
`vscode.LanguageModelError` carries typed `code` values
(`NoPermissions`, `Blocked`, `NotFound`, `RequestFailed`, …). The current
handler re-throws them all with the same generic message, so the user sees
cryptic strings like `Copilot error (NoPermissions): …` with no actionable
guidance.

Additionally, if the model stream throws a plain `Error` (e.g. a network
timeout), it propagates up uncaught to `runExplain`, which shows it in the
status bar, but **the LRU cache is not written** β€” meaning every retry hits
the model again instead of surfacing a friendlier retry prompt.
Comment on lines +32 to +34
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

In section 1, the doc says a non-LanguageModelError propagates up uncaught to runExplain and is shown in the status bar. In the current implementation, runExplain catches all errors and calls vscode.window.showErrorMessage(...), not a status bar message. Please adjust this description to match the actual error handling path/UI.

Suggested change
timeout), it propagates up uncaught to `runExplain`, which shows it in the
status bar, but **the LRU cache is not written** β€” meaning every retry hits
the model again instead of surfacing a friendlier retry prompt.
timeout), `runExplain` catches it and surfaces the message via
`vscode.window.showErrorMessage(...)`, but **the LRU cache is not written** β€”
meaning every retry hits the model again instead of surfacing a friendlier,
cached retry prompt.

Copilot uses AI. Check for mistakes.

### Suggested fix
Map each `LanguageModelError.code` to a human-readable message and offer
retry / re-auth guidance.

---

## 2. `ExplanationProvider.explain` β€” empty `result` stored in cache
**File:** `src/explanationProvider.ts`

```ts
let result = '';
try {
const response = await model.sendRequest([prompt], {}, token);
for await (const chunk of response.text) {
if (token.isCancellationRequested) { break; }
result += chunk;
}
} catch (err) { … }

result = result.trim();
this.addToCache(key, result); // ← stored even when result === ''
return result;
```

### Issue
When the model returns an empty body (e.g. content-filtered, quota exhausted,
or the token was cancelled mid-stream), `result` is `''` after `.trim()`.
That empty string is written into the LRU cache and subsequently rendered as
an invisible ghost-text decoration. The next identical request returns `''`
from cache without ever hitting the model again, making it look like the
feature is broken for that code snippet.

### Suggested fix
```ts
result = result.trim();
if (!result) {
throw new Error('Copilot returned an empty explanation. Please try again.');
}
this.addToCache(key, result);
return result;
```

---

## 3. `codeLensProvider.ts` β€” double semicolon (typo / syntax smell)
**File:** `src/codeLensProvider.ts`, `regexBlockLenses`

```ts
const RE = isXml ? XAML_RE : CODE_RE;; // ← double semicolon
```

### Issue
While not a runtime error in TypeScript/JavaScript, this is a clear typo that
may confuse linters (`no-extra-semi`) and obscure any future edits on that
line. Should be removed.

---

## 4. `codeLensProvider.ts` β€” `isNoiseLine` comment-detection misses `//` mid-regex
**File:** `src/codeLensProvider.ts`, `isNoiseLine`

```ts
if (/^(\\/\\/|#|--|%%|;)/.test(trimmed)) { return true; }
```

### Issue
The pattern correctly identifies single-line comments *at the start of a
trimmed line*. However, import/use-statement guard placed **after** this check
can still let through lines that begin with a comment prefix from some
languages not listed (e.g. Lua `--`, Haskell `--`). These are unlikely to be
active languages today but the `sourceDoc.languages` config is user-editable,
so arbitrary language IDs can be added.

Comment on lines +94 to +108
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

Section 4’s explanation/examples don’t match the current isNoiseLine implementation: the regex already includes --, so the examples β€œLua --, Haskell --” aren’t actually missed. Also the heading mentions β€œmisses // mid-regex”, but the snippet shown is anchored to the start of the trimmed line and does match // prefixes. Please revise this finding to reflect a concrete, reproducible gap (or remove it).

Copilot uses AI. Check for mistakes.
---

## 5. `extension.ts` β€” `languageRegistrations` never pushed to `context.subscriptions`
**File:** `src/extension.ts`

```ts
let languageRegistrations: vscode.Disposable[] = [];
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('sourceDoc.languages')) {
languageRegistrations.forEach(d => d.dispose());
languageRegistrations = [];
registerCodeLensProviders(context, codeLensProvider, languageRegistrations);
}
}),
);
```

### Issue
`registerCodeLensProviders` pushes the new `CodeLensProvider` registrations
into the local `languageRegistrations` array (correctly). However, that array
is **never added to `context.subscriptions`**. If the extension is deactivated
between two configuration-change events, the registrations accumulated in that
array will **not** be disposed. This leaks the provider registrations.

The first call to `registerCodeLensProviders` (outside the listener) correctly
passes `context.subscriptions` as the default, so only the _dynamic_
re-registration path is affected.

### Suggested fix
```ts
registerCodeLensProviders(context, codeLensProvider, languageRegistrations);
// After re-registration, track new disposables:
languageRegistrations.forEach(d => context.subscriptions.push(d));
```
Or simply push the `languageRegistrations` array reference once so VS Code
drains it on deactivation.

Comment on lines +142 to +146
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

Section 5 suggests β€œpush the languageRegistrations array reference once so VS Code drains it on deactivation”. context.subscriptions only disposes items that implement Disposable; pushing an array won’t be disposed. If you want a single subscription, wrap the array in a Disposable (e.g., a disposer that iterates and disposes the current registrations) or use Disposable.from(...) when registering.

Copilot uses AI. Check for mistakes.
---

## 6. `extension.ts` β€” `explainFile` does not guard against zero non-noise lines
**File:** `src/extension.ts`, `sourceDoc.explainFile` command handler

```ts
const results = await runWithConcurrency(lines, 5, async ({ line, code }) => { … });
```

### Issue
`runWithConcurrency` is called with `Math.min(limit, items.length)` workers.
When `lines` is empty (e.g. a file that consists entirely of comments or
blank lines), `items.length === 0`, which causes
`Array.from({ length: 0 }, worker)` β€” effectively a no-op. The progress
notification is shown and dismissed with `"0 / 0 done"` which is confusing.
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

Section 6 claims the progress UI shows/dismisses with "0 / 0 done" when there are no non-noise lines. In extension.ts, the progress title is Source Doc: explaining ${lines.length} lines… and the progress.report message is only set inside the per-line loop, so with 0 lines the user will more likely see β€œexplaining 0 lines…” (briefly) rather than β€œ0 / 0 done”. Please update the doc to describe the actual behavior you observe from this code path.

Suggested change
notification is shown and dismissed with `"0 / 0 done"` which is confusing.
notification is created with the title `Source Doc: explaining 0 lines…` and is dismissed without any per-line `"N / total done"` updates, which can look like a brief, confusing flicker.

Copilot uses AI. Check for mistakes.

### Suggested fix
Add an early exit and informational message:
```ts
if (lines.length === 0) {
vscode.window.showInformationMessage('Source Doc: nothing to explain in this file.');
return;
}
```

---

## 7. `codeLensProvider.ts` β€” `provideCodeLenses` accesses `document.lineAt(0)` without checking `lineCount`
**File:** `src/codeLensProvider.ts`, `provideCodeLenses`

```ts
const firstRange = new vscode.Range(0, 0, 0, document.lineAt(0).text.length);
```

### Issue
If an empty file is opened (`lineCount === 0`), calling `document.lineAt(0)`
throws a `RangeError`. VS Code normally guarantees at least one line, but
virtual/untitled documents and some language servers surface edge cases.
Comment on lines +182 to +184
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

Section 7 motivates the document.lineAt(0) guard using β€œvirtual/untitled documents”, but this CodeLens provider is registered with { scheme: 'file' }, so it won’t be invoked for untitled/virtual documents. If you want to keep this item, please reframe it as a general defensive guard (or cite a concrete case where a file-scheme TextDocument can have lineCount === 0).

Suggested change
If an empty file is opened (`lineCount === 0`), calling `document.lineAt(0)`
throws a `RangeError`. VS Code normally guarantees at least one line, but
virtual/untitled documents and some language servers surface edge cases.
`provideCodeLenses` assumes that `document.lineCount > 0` and calls
`document.lineAt(0)` unconditionally. VS Code currently guarantees at least
one line for normal `file`-scheme documents, so this should not occur in
typical operation, but it still relies on that invariant β€” if it is ever
violated (for example by an extension host or language feature bug that
surfaces a `file` document with `lineCount === 0`), a `RangeError` would be
thrown. A small `lineCount` check makes the code resilient to such edge
cases at essentially zero cost.

Copilot uses AI. Check for mistakes.

### Suggested fix
```ts
const firstRange = document.lineCount > 0
? new vscode.Range(0, 0, 0, document.lineAt(0).text.length)
: new vscode.Range(0, 0, 0, 0);
```

---

## Priority Matrix

| # | File | Severity | Type |
|---|------|----------|------|
| 2 | `explanationProvider.ts` | πŸ”΄ High | Empty response cached β†’ silent broken state |
| 1 | `explanationProvider.ts` | 🟠 Medium | Poor error messaging for LM errors |
| 5 | `extension.ts` | 🟠 Medium | Disposable leak on config change |
| 7 | `codeLensProvider.ts` | 🟠 Medium | Potential `RangeError` on empty file |
| 6 | `extension.ts` | 🟑 Low | Confusing UX on all-comment files |
| 4 | `codeLensProvider.ts` | 🟑 Low | Noise-filter gap for user-added languages |
| 3 | `codeLensProvider.ts` | βšͺ Trivial | Double semicolon typo |

---

## Reproduction Steps (items 1 & 2)

1. Open a TypeScript/JavaScript file.
2. Ensure GitHub Copilot is installed but temporarily sign out (or exhaust the
free-tier quota).
3. Click any **Explain** CodeLens.
4. Observe: an error notification appears with a raw `LanguageModelError` code.
5. Sign back in and click the same lens β€” **no explanation appears** because
the empty string was already cached from step 4.

---

*Investigation authored via automated static analysis of
[moonolgerd/source-doc](https://github.com/moonolgerd/source-doc) β€” `main`
branch @ `15e0c2f`.*