feat: ✨ Hover and go-to-definition on keys in .env buffers - #353
Conversation
Watson-Branch: #341
Both handlers returned before any dispatch on a `.env` buffer:
`goto_definition` gated on `.php`, `hover` on `.blade.php`/`.php`. Four
other env features already classified files through the shared
`env_key_locator::is_env_file_name` gate; these two now join them, and
branch to a dedicated env path ahead of the PHP pattern index rather than
falling through it into `Ok(None)`.
- hover on a key renders its name, effective value, declaring file (via
the existing `.env`/`.env.local`/`.env.example` priority ladder) and
consumer count. Commented-out and not-defined states mirror
`hover_for_env`; the not-defined card keeps its count, which is the
point of hovering a key the project cannot resolve.
- go-to-definition jumps to the consuming `env('KEY')` call sites — every
one of them, as a `Scalar` for a single hit and an `Array` otherwise.
A key with no consumers returns `None` where hover still renders.
- `enumerate_commented_keys_in_source` finds a key on a `#` line, which
`enumerate_keys_in_source` classifies as "not a declaration". Both
delegate to the same `parse_key_declaration`, and stay separate so a
commented line cannot win the code lens's first-match-wins race.
- the reference-count phrasing and the de-duplicated location lookup move
into `reference_count_label` / `reference_locations`, so a key's lens
count and its hover count come from one call and cannot disagree.
`textDocument/references` deliberately keeps its `.php`/`.blade.php`
gate: the reference code lens stays the only "find consumers" entry point
for env keys, as it is for config and translation keys. Pinned by a test.
Docs: `environment.md` claimed hover and go-to-definition "only ever run
on `.php` and `.blade.php`", which this change falsifies — corrected, with
the surrounding argument (a real `.sh` gets nothing) left intact.
Watson-Branch: #341
… separator Two fixes the merge with main surfaced. `hover_for_env_declaration` rendered the value raw. `hover_for_env` — the same card for the reverse direction — drops a value whose *name* matches `is_sensitive_env_name` and masks URL credentials otherwise (#344, #348). That the value is already on screen in the buffer is not a reason to skip the guards: this is LSP output like any other surface those issues swept. Both guards now apply, with a test for each arm. `env_value_redaction` (new on main in #348) compared a completion detail against a literal `config/app.php`. The detail renders a display path with the platform separator, so Windows produced `config\app.php` and reddened the whole matrix job on a difference that has nothing to do with redaction. The expectation is built from `Path::join` instead; the assertion still pins the redaction string itself. Watson-Branch: #341
There was a problem hiding this comment.
🔄 Changes Requested
All twelve acceptance criteria are met, and I checked each against the tree rather than reading your checklist. CI is green on four platforms. The two AC premises you flagged are both correct, and flagging them beat diverging silently — I've confirmed each below. What blocks is a case none of the twelve criteria names and no test covers: a key declared twice in one file, once active and once commented.
Issues Found
1. 🔴 The hover card can describe a different declaration than the one under the cursor
env_key_at_position does precise work — it resolves the cursor to an exact (line, column-range) in an exact file, keeping active and commented enumerations separate. Then hover_for_env_declaration throws all of it away and passes forward the bare name:
async fn hover_for_env_declaration(&self, key: &str, references: usize) -> String {
let var = self.salsa.get_parsed_env_var(key.to_string()).awaitlaravel-lsp/src/main.rs:21187, called at :21259.
get_parsed_env_var is name-keyed and priority-merged, and three facts in untouched code combine badly with that:
parse_env_sourceregisters commented lines too, withis_commented: true, into one line-ordered vec —salsa_impl.rs:1522-1560.register_env_files_with_salsastamps one priority per file (main.rs:6076-6088), so two declarations of a key inside one file always tie.- The tie-break keeps the first-seen entry —
salsa_impl.rs:11807:Some(existing) if existing.priority >= data.priority => {}
So on a tie the textually-first line wins, and the cursor plays no part in it. Both directions break:
.env="# APP_NAME=Old\nAPP_NAME=New\n"— hover the active line. The card renders*(commented out)*, with the source link from the commented entry. A live declaration reported as disabled..env="APP_NAME=Acme\n# APP_NAME=Backup\n"— hover the commented line. The card rendersAcmewith no commented-out marker at all.
Commenting an old value out directly above or below the current one is ordinary .env editing, so this is reachable in normal use, not a contrived fixture.
This is a line the PR adds, and it also defeats AC #7's intent — a commented declaration is supposed to hover with an explicit commented-out state, and here it doesn't. The fix is to thread the state you already resolved: env_key_at_position knows which enumeration matched and on which line, so pass that through instead of re-deriving it by name. Keep get_parsed_env_var for the effective-value ladder in AC #4 — that part is right and should not change.
Please add the fixture that would have caught it: one file, one key, active and commented, hovered on each line.
2. 🔴 The #-stripping rule now exists in two places
enumerate_commented_keys_in_source (env_key_locator.rs:184-212) re-implements the trim → trim_start_matches('#') → trim classification that parse_env_source already does at salsa_impl.rs:1523-1528. Your own doc comment names the coupling — "matches parse_env_source's trim_start_matches('#'), so ## KEY=value reads as a declaration on both sides of the stack" — which is precisely the problem: it's an invariant held by two independent copies and a comment asking them to stay in step. Change one (require a space after #, say) and cursor hit-testing silently disagrees with what Salsa calls commented. Same root as #1 — the buffer-local and Salsa views of "commented" drifting apart. Extract the shared rule.
3. 🔴 Three test assertions are thinner than the criteria they stand for
All in env_key_navigation.rs, all in-PR:
.env.localis never exercised. AC #4 names the full.env(2) >.env.local(1) >.env.example(0) ladder; the priority test only spans the two extremes (:293-318). Your ownenv_priority()helper defines the middle tier at:43-49and no assertion ever uses it. Add.env.localas a middle tier that loses to.envand beats.env.example.- The zero-consumer card isn't checked for being a card (
:320-342). AC #5 says a full hover card — key, value, source file,0 references— but the test asserts only the key name and the count. Drop the value and the source link in that branch and it still passes. - The commented-key test never asserts the value is absent (
:384-410). Its sibling redaction test does exactly this (!card.contains("hunter2"),:551-582). Worth matching, especially since finding #1 shows the commented branch is reachable with the wrong entry.
What's Good
The refactor is the strong part of this PR. Extracting reference_count_label and reference_locations and routing both code_lens_resolve and the new hover through them means a key's lens count and its hover count are now the same call — they cannot drift apart, which is the right way to satisfy "not a new wording."
The redaction work is genuinely careful. hover_for_env_declaration mirrors hover_for_env's guard chain exactly — commented → sensitive-name → URL-credential masking, same order, no branch reaching a raw value — and the reasoning you left in the comment ("the value being on screen already is not a reason to skip them") is the correct instinct. LSP output is LSP output.
The test harness deserves credit for its honesty: cursor_off_the_key_resolves_nothing carries an explicit positive control so the negative assertions can't pass vacuously through a gate that rejects everything. That's the kind of thing that's easy to skip and expensive to miss. Nine self-applied mutations, nine killed, is a real check and not a claim.
Both AC premises confirmed. is_env_file_name's body is the AC's literal expression (env_key_locator.rs:49-50), so calling it is the stronger reading — no second spelling to drift. And get_parsed_env_var(name: String) takes no position and no file (salsa_impl.rs:7756), while get_all_parsed_env_vars merges across files, so neither can answer "which key is at this cursor in this file." Bullet 3's named mechanism genuinely cannot do positional lookup; reading the buffer was right. Note the irony that finding #1 is the same lesson applied one layer further in — you correctly refused a name-keyed lookup for position, then used one for state.
📋 Non-blocking follow-ups
buffer_text(main.rs:21144-21149) reads from disk without thepath_within_rootcheck used atmain.rs:5988-5997. Its path comes from the request URI, not a project-wide index, and the identical fallback already exists verbatim for PHP/Blade hover atmain.rs:24539-24542— so this matches the repo's existing pattern rather than departing from it. Noted — not tracked.env_value_redaction.rs:462-502carries an unrelated Windows path-separator fix. Correct in itself, just not this PR's subject. Not worth the churn of splitting out. Noted — not tracked.
(Watson: findings 1-3 are the blockers — all three belong to this unit, please fold them into this PR. The two items in this section are unrelated and optional; neither is tracked and neither needs action.)
Please address the above and re-request review.
`enumerate_commented_keys_in_source` re-implemented the trim →
`trim_start_matches('#')` → trim classification `parse_env_source` already
does, and its doc comment asked the two copies to stay in step. An invariant
held by a comment is an invariant that drifts: change one side (require a
space after `#`, say) and cursor hit-testing silently disagrees with what
Salsa calls commented.
`commented_declaration_body` is now the one definition, and every reader of a
`.env` line classifies through it — Salsa's `parse_env_source`, the
declaration parser that must reject exactly those lines,
the commented-key enumeration that parses what they hide, and the
inline-comment tokenizer.
The rule is unchanged, so no behaviour moves: same `#` run, same whitespace,
same body. Direct tests pin it — the marker run, the non-comment shapes, and
the suffix property the column arithmetic rests on.
Watson-Branch: #341
…y name
`env_key_at_position` resolved the cursor to an exact line in an exact file,
then `hover_for_env_declaration` threw that away and asked
`get_parsed_env_var` — a name-keyed, priority-merged lookup — what state the
key was in. Every declaration inside one file carries that file's priority, so
two declarations of one key always tie, and the tie kept the textually-first
line. The cursor played no part.
Both directions were wrong for the ordinary habit of commenting an old value
out beside the live one:
- `# APP_NAME=old` above `APP_NAME=new` — hovering the live line rendered
*(commented out)*, a live declaration reported as disabled.
- `APP_NAME=new` above `# APP_NAME=old` — hovering the comment rendered the
live value with no commented-out marker at all.
The state now comes from the cursor: `env_key_at_position` returns which
enumeration matched, and a commented declaration needs no lookup at all — it
is the line under the cursor, in this buffer, with no value in effect and its
own file to link. The priority ladder still resolves an active declaration's
value and declaring file, as the criteria ask.
The tie-break itself was the mechanism, so it is fixed where it lives: at
equal priority an active declaration now outranks a commented one, in both
env merges, through one shared `env_var_supersedes`. A comment is not a
declaration competing for the key. A commented declaration in a
higher-priority file still wins — switching a key off in `.env` is how a
project disables it — and hovering an active declaration it outranks now
reports no value in effect rather than quoting one the application never
sees.
Tests: the fixture pair Holmes asked for (one file, one key, active and
commented, hovered on each line, both orderings), the reverse `env('KEY')`
direction and the whole-table merge on the same fixture, the commented card's
own source link, and the outranked-by-a-comment case. Three thin assertions
sharpened: the priority test now spans all three rungs and its middle tier
alone, the zero-consumer card asserts value and source link (its fixture key
matched the sensitive-name pattern, so it never carried a value at all), and
the commented card asserts the value is absent.
Watson-Branch: #341
|
Blockers fixed (all three belong to this unit); unrelated cosmetics skipped — both were marked Noted — not tracked, and I left them alone. 1. The card described a declaration other than the one under the cursor. State now comes from the cursor: I also fixed the mechanism, not only the reported field. The tie-break kept the textually-first entry, so the value had the same defect as the state: 2. The 3. The three thin assertions. The priority test spans all three rungs, and a second test exercises the middle tier alone ( New fixtures: the pair you asked for (one file, one key, active and commented, hovered on each line) in both orderings; the reverse Mutations: 10 applied, 10 killed — state re-derived by name (3 tests red), tie-break reverted (2), the commented-winner guard dropped (1), the shared rule stripping one Sweep. The class is "who classifies a Full suite green (3,417 tests), clippy clean, |
There was a problem hiding this comment.
🔄 Changes Requested
All three round-1 blockers are genuinely fixed — I checked each against the tree rather than against your summary, and the fix went further than the report I asked for. All twelve acceptance criteria are met. CI is green on four platforms. What's left is one assertion, on a fixture you already wrote.
Issues Found
1. 🔴 The new tie-break's first-wins guarantee isn't pinned by any test
env_var_supersedes (salsa_impl.rs:11801-11806) is the heart of this round's fix, and its Equal arm has four input rows. Two are covered; two aren't, and one of those matters:
Ordering::Equal => current.is_commented && !candidate.is_commented,| current | candidate | correct | covered by |
|---|---|---|---|
| commented | active | true |
a_commented_line_above_the_active_one_does_not_answer_for_it |
| active | commented | false |
a_commented_line_below_the_active_one_still_reads_as_commented |
| active | active | false |
nothing |
| commented | commented | false |
nothing |
Mutate && to || and both covered rows return exactly what they returned before — so the whole suite stays green — while row 3 flips from first-wins to last-wins. That is the same defect class this round exists to close: enumerate_keys_in_source is first-match-wins, so the cursor only ever resolves on the first KEY= line; if the merge went last-wins, hovering that first declaration would render the second declaration's value. A card describing a line other than the one under the cursor — round 1's finding #1, arrived at from the other direction.
The fixture is already there. second_declaration_of_the_same_key_resolves_nothing (env_key_navigation.rs:264-288) builds exactly the right file:
&[(".env", "APP_NAME=first\nAPP_NAME=second\n")],and then asserts only hover_at(&env, position(0, 2)).await.is_some() — that something came back, never that it was first. Assert the winning value on that first line (and that second is absent) and the mutation dies. Row 4 (both commented) I'd leave alone: the commented branch is cursor-derived and renders no value, so no output distinguishes the two, and a test would be asserting into a void.
Your mutation round covered "tie-break reverted" — that restores existing.priority >= data.priority, which is row 1's behaviour, so it kills row 1 and leaves row 3 standing. Worth noting because the sweep was otherwise thorough: this is a gap in the mutation set, not in the effort.
What's Good
The fix is better than the one I asked for. I flagged the state being re-derived by name; you found that the value had the identical defect through the same tie-break, and fixed the mechanism rather than the symptom — # APP_NAME=old above APP_NAME=new would have rendered old on the live line even with the state correctly threaded. Fixing the cause you weren't asked about is the difference between closing a bug and closing a bug class.
The cross-file restraint is the right call and correctly reasoned. Keeping a commented .env declaration ahead of an active .env.local one, because commenting a key out in .env is how a project switches it off, preserves the ladder's meaning; scoping the new rule to equal priority means it can only ever fire within one file, since register_env_files_with_salsa hands out 0/1/2 uniquely. Both merge call sites route through the one helper, so they cannot drift.
commented_declaration_body holds on both sides — I traced ##KEY=x, #KEY=x, and # # KEY=x through Salsa and the buffer enumeration and they agree on all three, the last rejected by both via the space in the name. The suffix property the column arithmetic rests on is real and you pinned it directly.
The sweep is accurate, and the judgement calls in it are the ones I'd have made. I ran the class myself: the remaining # classifiers in src/ are config.rs's PHP-array parsers, database.rs's gitignore and docker-compose readers, and livewire_config.rs — none of them read .env text. Naming the integration_tests.rs site and your reason for leaving it is exactly the visible-judgement I want instead of a silent skip.
Catching that UNUSED_KEY matched the sensitive-name pattern — so the zero-consumer card never carried a value and the old assertion couldn't have failed — is the kind of thing only a real strengthening of an assertion surfaces.
📋 Non-blocking follow-ups
- A key declared only in a
.env.*variant outside the registered ladder (.env.testing,.env.staging) renders*(not defined in .env)*until that buffer is edited, becausepath_is_env_fileadmits it for dispatch butregister_env_files_with_salsa(main.rs:6073-6089) registers three files. Pre-existing, unrelated to this unit, and no AC bullet contemplates variants beyond the ladder. Noted — not tracked. env_var_supersedes's docstring says "every declaration inside one file carries that file's priority and so always ties." With two non-.env/.env.localvariants open at once, two different files also tie at priority 0, and the fallback then rests onHashMapiteration order. Pre-existing and untouched by this PR — the docstring just claims a little more than the ladder guarantees. Noted — not tracked.buffer_text(main.rs:21141-21149) still reads from disk withoutpath_within_root. Unchanged from round 1, still matching the repo's dominant pattern rather than departing from it. Noted — not tracked.
(Watson: finding 1 is the blocker and it belongs to this unit — one strengthened assertion on a fixture that already exists. The three items above are unrelated to this unit and none is tracked; skip them.)
Please address the above and re-request review.
`env_var_supersedes`'s equal-priority arm has four input rows. The two mixing a comment with a live line were pinned; two *active* declarations of one key were not. Mutating that arm's `&&` to `||` therefore left the whole suite green while first-wins silently became last-wins — and `enumerate_keys_in_source` only ever resolves the cursor onto the first `KEY=` line, so hovering that line would have rendered the second declaration's value. A card describing a line other than the one under the cursor: the defect the previous commit closed, reached from the other side. The fixture for it already existed. It asserted only that the first line resolved *something*, never which declaration answered; it now asserts the first declaration's own value is on the card and the second one's is not. The table merge gets the same tie pinned as well. It already carried the comment-loses row, so leaving first-wins on the by-name side alone would have left one of two call sites of the shared rule free to drift. Mutation-verified live, all three forms the arm can take: - `||` (first-wins becomes last-wins) — both new assertions red, and only those two, which is what made this a gap rather than incidental coverage. - `false` (the pre-fix always-first-wins) — the three comment-loses tests red, the new pair green, so they discriminate on their own row. - `true` (always last-wins) — the comment-below test and both new assertions red. Row four, two commented declarations, stays unpinned deliberately: the commented branch is cursor-derived and renders no value, so no output tells the two apart. Refs #341
|
Blocker fixed — it belongs to this unit. The three follow-ups were all marked Noted — not tracked and unrelated to the unit, so I left them alone. The tie-break's first-wins row is now pinned. You were right that the fixture was already there and asserted only that something came back. I pinned the same row on the other merge too. Row 1 was covered on both call sites — Mutations: 3 applied live, 3 killed. Your
The new pair stays green under The class, not just the instance. The defect shape is "asserts that something resolved, never which record answered." Two Doc-drift. The Test Plan's counts were stale from round 1 — 12 tests and a 2653/592/80 suite. Corrected to the measured 21 + 7 and 2692/646/80, with this round's mutations listed and rounds 1 and 2 cited as the thread's record rather than folded into a total I did not re-run. Full suite green, |
There was a problem hiding this comment.
🔄 Changes Requested
Round 2's blocker is genuinely closed, and you closed it on both call sites rather than the one I named. All twelve acceptance criteria are met. CI is green on four platforms, and I had the suite run independently rather than take the number on trust — 2692 + 646 + 80, zero failures, exactly your counts. Production code is byte-identical to last round (git diff --name-only 3c87cb6 b65e340 returns one test file), so the twelve criteria I verified then still hold now.
What blocks is something neither of us has looked at in three rounds: the card renders the key name as markdown, and a .env key is not an identifier.
Issues Found
1. 🔴 A .env key is rendered as unescaped markdown, and key syntax has no charset
parse_key_declaration defines a key as everything before the first =, whitespace-trimmed — env_key_locator.rs:234-249. There is no character restriction; I grepped for one and there is none. That string then reaches the card as a header:
header: Some(key),main.rs:21221, :21248, :21261, :21272 — all four branches of hover_for_env_declaration. And hover::render interpolates a header raw:
sections.push(format!("**{}**", h));hover.rs:149, with the result returned as MarkupKind::Markdown (main.rs:21294-21295).
So a line like
[Update your credentials here](https://evil.example/harvest)=1
parses as a declaration whose key is the whole bracket-paren expression, and hovering it renders a live clickable link inside the hover card. docs/hover.md:95 confirms the client treats card content as click-to-open. An =1 variant needs no click at all.
This is in-PR, and it is new. hover_for_env — the pre-existing reverse direction — never populates header at all (main.rs:21070-21102); it uses only detail, code, source_link and trailer. The other two header: call sites in the file (:20242, :24680) are fed class and member names from extract_class_fqn/extract_class_signature, which are PHP identifiers and cannot contain [, ], or a backtick. Those four new lines are the first place in this codebase that puts genuinely unconstrained text into that field, and hover.rs is untouched by this PR — so the renderer never had to be safe before, and now it does.
Reachability is ordinary, not contrived: .env.example is committed to repositories, and opening an unfamiliar repo starts the LSP without asking. This is the same family as #344 and #348 — what the LSP puts on screen from .env content — approached from the other side. Those two asked "can the value leak?"; this asks "can the key act?"
Sweep the class before you fix it, because the class has two sites. The invariant is unconstrained .env text reaching markdown unescaped. The other site is hover.rs:157-160:
CodeLanguage::Plain => format!("```\n{}\n```", code.content),— no fence-length negotiation, so a value containing three backticks closes the fence early and everything after it renders as markdown. That one is pre-existing and hits hover_for_env identically, so I am not asking you to own it as a defect; I am asking you not to fix the header in isolation and leave its twin one function away. Closing this at the renderer closes both. Closing it at the four call sites closes one, and I would rather you make that choice deliberately than discover the second site next round.
No test in the 21 exercises a key containing a markdown metacharacter. Whatever fix you pick, that fixture is the one that pins it.
2. 🔴 Three test comments claim more than the tests assert
All in env_key_navigation.rs, all in-PR, and all the same shape — prose that describes a stronger test than the code underneath it.
- The module doc is now false. Lines 10-15 say "Every test drives the real
LanguageServer::hover/LanguageServer::goto_definitionthrough atower_lsp::LspServiceharness, not the helpers underneath." Three of the 21 do exactly the opposite:the_reverse_direction_reads_the_same_pair_the_same_way(:626) callsserver.hover_for_env(...), and both table-merge tests (:649,:672) callserver.salsa.get_all_parsed_env_vars(). None of the three toucheshover_at/goto_at. The tests themselves are right and I asked for the second of them — the merge call sites should be pinned at the Salsa layer, and their own comments say so honestly. It is the totalizing claim at the top that is now wrong, and it went wrong in two steps: two of those tests landed in round 2, the third in round 3, and I missed it both times. Scope the sentence to the navigation tests. hover_in_a_lower_priority_file_shows_the_winning_valuepromises an assertion it never makes. Its comment (:308-312) says the card must "report the value the application actually runs with, and name.envas the declaring file." The body asserts three values and no link (:326-337). Its siblingenv_local_outranks_env_example_when_env_is_silentdoes assert the declaring file (:367-370), which is what makes the omission look like a slip rather than a decision. A mutation that resolved the right value through the ladder but linked the queried file instead of the winning one survives this test.contains("0 references")also matches"10 references".:399-402, and the same shape at:444,:480,:718,:763. Nothing in the current fixtures can produce a double-digit count, so no live mutation escapes — this is fragility, not a hole. I raise it here only because it is the same defect as the other two: an assertion that reads stronger than it is. A trailing delimiter or an equality check on the label settles it.
What's Good
The fix I asked for landed, and then went one better. I asked you to assert the winning value on a fixture that already existed; you also noticed that row 3 was covered on the by-name merge and not on the table merge, and that a shared rule pinned at one of two call sites is a rule waiting to drift at the other. the_whole_table_merge_keeps_the_first_of_two_active_declarations is the test I should have asked for and didn't.
The mutation table is the right way to answer a coverage claim. Running all three forms the arm can take — ||, false, true — rather than only the one I named is what demonstrates the new pair discriminates on its own row instead of riding row 1's cover. The false run is the load-bearing one and it is the one a weaker report would have skipped.
Declining to pin row 4 is correct and correctly argued: the commented branch is cursor-derived and renders no value, so there is no observable difference to assert against. Naming that out loud, with the reason, is better than a silent gap. Same for goto_definition_on_a_commented_key_still_jumps — I checked, and you are right that goto resolves through find_references, which never consults env_var_supersedes, so it has no tie to get wrong; its active-key sibling pins file and line on the same shape at :215-220.
Correcting the stale Test Plan counts rather than folding three rounds into one unverified total is the honest move, and the numbers check out against an independent run.
The two AC premises you corrected in round 1 both still stand on re-reading, and the AC review agrees: is_env_file_name's body is the criterion's literal predicate, and get_parsed_env_var is name-keyed so it cannot answer a positional question — the buffer-local companion is the only mechanism that can, and it reuses the same primitives rather than inventing a regex.
📋 Non-blocking follow-ups
- Two custom-suffix env files (
.env.testing,.env.staging) both land in the priority-0 bucket, so "first seen wins" resolves throughHashMapiteration order across files rather than content order —salsa_impl.rshandle_get_parsed_env_var. Pre-existing, unrelated to this unit, no AC bullet contemplates variants beyond the ladder. Same item I raised last round. Noted — not tracked. env_value_redaction.rs'sconfig_app_display()helper is an unrelated Windows path-separator fix riding along. Correct in itself, not worth the churn of splitting out. Noted — not tracked.buffer_text(main.rs:21141-21149) still reads from disk withoutpath_within_root, matching the identical fallback the PHP/Blade hover already uses atmain.rs:24590-24593— repo convention, not a departure. Third round unchanged. Noted — not tracked.
(Watson: findings 1 and 2 are the blockers and both belong to this unit — fold them into this PR. Finding 1 asks you to decide where the escaping lives before you write it, not just to patch four call sites. The three items above are unrelated to the unit, none is tracked, and none needs action.)
Please address the above and re-request review.
A `.env` key is everything before the first `=`, with no charset restriction, and the hover card rendered it as a bolded markdown header. A line spelling `[Update your credentials here](https://evil.example)=1` therefore put a live clickable link inside the card, and an `` variant needed no click at all. `.env.example` ships in public repositories, so opening an unfamiliar project is the whole of the reach. Close it at the renderer rather than at the four call sites that are known-unsafe today, so callers that do not exist yet are safe too. The class is "unconstrained `.env` text reaching markdown unescaped", and sweeping it found three renderer sites, not the two the review named: - `hover::render`'s bold header — the in-PR hole; - `hover::render`'s code fence, which a value carrying three backticks closes early (pre-existing, and it hits `hover_for_env` identically); - `CompletionDoc::render`, which has both shapes and is fed a raw key and a raw value by the `completion` handler's `.env` branch. `markdown_safety::escape_inline` defers its escape set to `char::is_ascii_punctuation`, whose ranges are exactly CommonMark's escapable set — a spec constant rather than a hand-listed set of "the characters that can start a construct", which is the enumeration that goes stale. `fenced_block` sizes each fence one backtick longer than the longest run inside its content. Rendered output is unchanged: CommonMark renders `\.` as `.`. The wire text gains backslashes, which is why 33 assertions across nine test files move to the escaped form — that spread is the sweep, showing which surfaces route through these two renderers. Field contracts are now explicit in both structs, so the boundary reads as a decision rather than an oversight: `header` is plain text and the renderer escapes it; `detail`, `description` and `summary` are markdown-bearing by design and a call site putting untrusted text there owns the escaping. Watson-Branch: #341
…romise
All three are prose that described a stronger test than the code under
it.
The module doc said every test drives the real `hover` /
`goto_definition` handler "not the helpers underneath". Three of the 21
do the opposite, on purpose: the reverse direction has no `.env`-buffer
handler to drive, and the two table-merge tests read the Salsa layer
because that is where the shared merge rule's second call site lives.
Scope the claim to the navigation tests and name the exceptions.
`hover_in_a_lower_priority_file_shows_the_winning_value` promised to
"name `.env` as the declaring file" and asserted three values and no
link, so a mutation that resolved the right value through the ladder and
then linked the *queried* file survived it. It now rules out both other
rungs by name, since `.env` is a substring of each. Verified live: with
`hover_for_env_declaration` linking `path` instead of
`var.source_file`, the test reddens.
`contains("0 references")` is also satisfied by `"10 references"`, and
`contains("1 reference")` by `"1 references"`. No live mutation escaped,
because no fixture reaches a double-digit count — this closes the shape
rather than waiting for the fixture that makes it real. All six count
assertions now route through one helper that builds the label from
`code_lens::reference_count_label`, so the pluralisation rule cannot
drift, and pins both ends of the match to a non-alphanumeric boundary.
The helper has its own test for the two escapes above.
Adds the fixture the review asked for: a key spelling a markdown link
and one spelling an image, plus a value carrying a fence, driven through
the real handler. Each was mutation-verified against the renderer change
it guards, including on the reverse direction's call site.
Watson-Branch: #341
|
Both blockers fixed in this PR. Unrelated cosmetics skipped; the three Finding 1 — where the escaping livesAt the renderer, per your steer. The invariant is "text this renderer was handed renders as itself", and only the renderer can hold that for callers that do not exist yet. Sweeping the class found three renderer sites, not two:
The cost, stated plainly. Escaping changes the wire text of every header carrying punctuation, so 33 assertions across nine test files move to the escaped form. Rendered output is byte-identical ( Finding 2 — three claims that now match their assertionsModule doc scoped to the navigation tests, with the three deliberate helper-level tests named and their reasons kept. The lower-priority ladder test now rules out both other rungs by name, since Added the fixture you asked for: keys spelling a link and an image, and a value carrying a fence, all through the real handler — plus the reverse direction's fence call site, on the same reasoning that made you want the table merge pinned at both sites. One thing I found and did not fix
|
|
Not moving this to review yet — the CI workflow never started for CodeQL ran on that exact SHA and passed. The Rather than hand you a PR whose gate never ran, I am leaving the item in What I verified locally, running exactly what CI runs:
The three-platform matrix is the one thing I cannot reproduce here. Nothing in this round is platform-sensitive — the change is string construction with no path or filesystem logic — but that is a reason to expect green, not a substitute for seeing it. Re-running the workflow from the Actions tab, or any new push to the branch, should enqueue it. |
Last round closed "unconstrained `.env` text reaching markdown unescaped" at both renderers, and wrote the boundary down: `header` is plain text the renderer escapes for every caller, `summary` is markdown-bearing and its call site owns the escaping. The `.env` branch of `completion` is the call site that hands `summary` untrusted text, and it escaped nothing. The round that documented the contract left the one site that violates it. A `.env` value has no charset restriction, any more than a key does. `SUPPORT_NOTICE=[Update your credentials here](https://evil.example)` therefore renders a live clickable link in the completion documentation panel, and the `` variant is fetched with no click at all. That is the reach the key had through the hover header, one field over, in the popup most likely to be open while the `.env` file is the one on screen. The escape wraps the whole `summary` expression, not its untrusted arm. All three arms here are plain text meant to render as themselves, so escaping the field rather than one branch of it means a fourth arm added later cannot reopen this. Two test helpers modelled the panel with an unescaped value and move to the escaped form. `assert_no_process_var_leak` now strips backslashes before its whole-response substring search: the summary is escaped, so a leaked value carrying punctuation no longer spells the raw needle there, and the "any field" reach that assertion claims would have quietly stopped covering it. That strip is not what catches the restored `std::env::vars()` loop — `label` and `detail` are plain text and still do — it is what stops the summary becoming a blind spot. The module doc now states the guarantee per field: the renderers cover the header and the code fence, and every other field is verbatim by design. Watson-Branch: #341
|
Round 4 blocker fixed and pushed as The fixBoth round-3 blockers were already closed on
The round that wrote the contract down left the one site that violates it. The header got escaped, the code fence got negotiated, and the value — which the commit message itself named as untrusted — travels through neither: the env branch never calls
Mutation: removing the escape reddens 13 tests, including The CI blocker
It is not configuration and not a code change: Locally, running exactly what CI runs:
The three-platform matrix is the one thing I cannot reproduce, and this round is string construction with no path or filesystem logic. That is a reason to expect green, not a substitute for it. This needs a hand now. The item stays |
`main` gained #357's config-cache work, which added `handle_invalidate_config_path` at the same insertion point this branch added `env_var_supersedes`. Adjacency only — both are kept, with the invalidator next to its `handle_invalidate_lang_path` sibling. No semantic overlap: #357 moves config lookups behind `TranslationCache`, and touches no env parsing, no env merge, and no card rendering. Swept `main`'s new `main.rs` lines for a fresh member of the markdown-escaping class this branch closes — no render site among them. Watson-Branch: #341
The `main` merge reddened `the_config_completion_response_carries_no_dotenv_secret` on Windows alone: the test expected `config\app.php` and got `config/app.php`. The test was right until this merge and is wrong after it. #336 moved the config label from a hardcoded `format!("config/{group}.php")` to `strip_prefix`, which picked up the platform separator; the commit now on `main` fixes that by routing the label through `with_forward_slashes`, because it is user-visible text that must not change shape with the host OS. `config_source_label`'s own doc names the old Windows rendering as the bug. So `config_app_display()` — which built the expectation with `Path::join(...).display()` — encoded the pre-fix behaviour, and its comment ("Windows displays `config\app.php`, which is correct for Windows") now asserts the opposite of what production guarantees. Replaced with a literal `CONFIG_APP_LABEL`. The literal is the stronger assertion, not merely the passing one: the helper mirrored the production logic, so if the normalization were ever dropped the expectation would pick up the native separator alongside it and stay green on Windows — the only platform where that regression is visible. A literal cannot follow it. Mutation: setting the constant to `config\app.php` reddens the test with exactly the CI failure inverted (`left` and `right` swapped), so the assertion discriminates on separator shape rather than merely being reached. Swept the class — `config_app_display` had no other call site, and no sibling test in `src/tests` or `tests/` builds a user-visible expectation through `display()`/`to_string_lossy()`. Watson-Branch: #341
|
CI is green on all four platforms, and the reason it would not start is found. Moving to review. Both round-4 blockers were already fixed and pushed as Why
|
|
🛑 Escalating to @mikebronner — this review still found a blocker, and it's the 4th round of changes since your last input on this PR. I reviewed the current head ( Issues Found This Round1. 🔴 A secret-leak assertion this PR made blind, in the same file where its sibling got the fix
assert!(
!json.contains(SECRET_VALUE),Its sibling let unescaped = json.replace('\\', "");
assert!(!unescaped.contains(SECRET_VALUE),with a comment naming the mechanism exactly: "The documentation panel's summary now arrives markdown-escaped, and a value carrying punctuation spells That reasoning applies verbatim to line 479 and wasn't carried there. This PR is what caused it. Before It's also the third round in a row where the finding is "the class was swept at one site and not the one beside it." Round 3 was the markdown header without its code-fence twin; round 4 was the contract written down with the completion panel left violating it; this is the escaping's blast radius on assertions, handled in the helper and not in the standalone. The instinct is right every time and lands one site short. What Didn't Survive VerificationTwo findings came in as blockers and I dropped them. Recording them so the round is legible:
What's GoodThe merge diagnosis is the standout. Three rounds of the CI symptom went unexplained, two of my own theories included, and the answer — the branch was behind Replacing Finding the third member of the escaping class yourself — And the honesty holds up under checking. The Your CallThe PR is one line from done. Options as I see them:
My recommendation: option 2. It's a single line with an existing model directly above it, it closes a security assertion this PR is what blinded, and it's a smaller cost than the tracking overhead of option 3. But this is squarely your call — the work is good, the gap is small, and I've now bounced it three times. Any input from you resets the strike window; the next review starts fresh instead of escalating on sight. |
|
Please fix this. |
Round 5 fixed `assert_no_process_var_leak` to strip backslashes before searching, because this PR's `escape_inline` on `CompletionDoc::summary` spells a punctuated value `s3cr3t\-value\-set…` there. The reasoning was written at that one assertion and carried nowhere else, so two sibling searches were left hunting a needle the panel no longer spells: - `env_completion_system_leak.rs`, the shadowing test's standalone check (the site Holmes flagged); - `env_value_redaction.rs`'s `assert_no_secret_leak`, the same helper shape in another file, blind across all four of its needles — every one carries hyphens. Both files now name the strip as `searchable()` so the reasoning reaches every call site instead of one, and both helper docs' "any field" claim is true again. Proven live, not by inspection. A summary-only leak (the `.env` completion summary made to answer from the real value, `detail` left redacted) went entirely unnoticed by both searches before this change — the tests failed only later, on a panel-equality assertion. After it, each fails on the leak assertion itself: `env_value_redaction.rs:193` and `env_completion_system_leak.rs:488`. The strip is itself unexercised by a green suite — a leak has to exist before the escaping can hide one — so deleting it would have degraded silently, the same shape one level up. Each file now pins it at its own definition, over its own needles, and each fixture reddens alone when the strip is removed. Their first assertion fails if a needle ever loses its ASCII punctuation, which is what stops the second going vacuous. Scope checked and left alone: the hover surface renders values through `fenced_block`, which lengthens the fence and never backslashes content, so its raw-needle assertions still match; `escape_inline` reaches only the two headers and this one summary. Every other negative assertion in the tree either searches a field this PR does not escape or uses a needle with no ASCII punctuation, which escaping leaves byte-identical. Watson-Branch: #341
|
Mike picked option 2, so here is the one line — and the sibling it turned out to have. The flagged site
The one beside itYou named the recurring shape as "the class was swept at one site and not the one beside it", so I treated the fix as a class and swept for every leak search this PR's escaping could have blinded. There is a second, in a different file: That file was not in your review and not in the round-4 fix. Fixing only line 479 would have landed one site short for the fourth round running. Proven live, not by inspectionI made the
The shadowing test needed its own mutation — my first one leaks the The strip needed pinning tooDeleting Where I stopped, and why
One judgement call worth your pushback: Full suite green: 3,451 tests, I also corrected a factual claim in my own new prose before pushing — a doc comment said the two searches sat "twelve lines" apart when they are roughly three hundred. |
There was a problem hiding this comment.
✅ Approved
Mike's call was option 2, and you took the one line — then swept the class it belonged to and found the site I'd missed. That's the right order of operations, and it's the first round in five where the sweep got ahead of me instead of one site behind.
Review Summary
- Reviewed head
65495a4. All twelve acceptance criteria met, CI green on all four platforms plus both CodeQL analyses. - Four blind lenses (AC conformance, correctness, security, test-honesty). Three returned no findings. The correctness lens ran the suite itself — 665/665.
- One finding was promoted to blocker-class by me and sent to the full red-team / blue-team / auditor panel, since
CHANGES_COUNTreset to 0 when you weighed in. It was refuted. Detail below, because a split panel deserves to be legible.
The env_value_redaction.rs find was the right call
You were asked for line 479 and delivered the class. assert_no_secret_leak was blind across all four of its needles, in a file that was not in my review and not in the round-4 fix — so the one-line fix I recommended would have landed one site short for the fourth round running. My recommendation was the narrow one; yours was correct.
I verified the scope claim independently rather than taking it: escape_inline has exactly three production call sites — completion_format.rs:216, hover.rs:163, main.rs:28050 — which is precisely "the two headers and this one summary." I also checked the three other test files carrying negative assertions. helper_identifier_hover.rs:194 and :227 search punctuated needles, but they read source_link, which markdown_safety's own module doc names verbatim-by-design and never escapes. Nothing else is blinded. Your boundary holds.
Pinning searchable at its own definition, over its own needles, with a first assertion that reddens if a needle ever loses its ASCII punctuation — that's the fix for the vacuity one level up, not just the vacuity you were sent to fix.
The one finding I promoted, and why it didn't survive
The test-honesty lens noted the new buffer_text disk fallback (main.rs:21168) has no test — every fixture pre-populates documents. The lens called it non-blocking; that routing is mine, not the lens's, and an untested new branch in the PR's own code is in-pr, so I sent it to the panel.
The panel split, which is exactly when the third seat earns its keep:
- Red team: exploitable. Closed reachability graph —
buffer_text←env_key_at_position←env_key_hover/env_key_definition. Onlyenv_key_navigation.rsdrives those handlers, every fixture populatesdocuments, nodid_closeanywhere. Mutating the arm leaves the suite green. - Blue team: mitigated. Byte-identical to a pre-existing fallback in
hover(), and the idiom recurs 20+ times in the file, none pinned. - Auditor: refuted, and I confirmed its pivotal fact myself rather than trusting the report.
git blameputsmain.rs:24626atcda1a3b6, 2026-05-17 — three months old.gh pr diff 353contains exactly oneread_to_string(path).unwrap_or_default()line, yours; the sibling isn't in the diff or even in a hunk's context window.
The failure mode is benign end to end: empty string → no keys from either enumerator → env_key_at_position returns None → propagates via ? to a declined hover. No panic, no wrong data. Holding this PR to a bar the surrounding code has never met, for a provably non-behavioural branch it inherited rather than introduced, is scope creep onto pre-existing debt — not a blocker.
AC 3 — met by divergence, and the divergence is the reason it works
The AC-conformance lens flagged AC 3's wording as defective. I checked that myself before deciding, because "the AC is wrong" is Mike's call and not mine to make casually.
It's met, by a divergence that is a strict improvement:
handle_get_parsed_env_var(&self, name: &str)(salsa_impl.rs:11936) is name-keyed. You need the key to call it — and the key is what a position lookup is trying to find. The AC's literal route can't answer the question it was asked to answer.- The criterion's real intent — no ad-hoc regex, one notion of "commented" — is satisfied better.
salsa_impl.rs:1526now calls the sharedenv_key_locator::commented_declaration_body, so Salsa's parser and the buffer-local enumerator provably cannot drift. - Following the literal wording would have reintroduced my round-1 blocker: the merged Salsa table keeps only the winning file's entry, which is how a hover card ends up describing a declaration the cursor isn't on.
Nothing dropped, strictly better, and already adjudicated on the record in round 1 when you flagged the premise instead of diverging silently. That was the right move then and it's why this isn't an escalation now.
On your judgement call
You asked for pushback on searchable being a file-local copy in each of two files rather than a shared helper. Keep it as is. dissect is already duplicated between those same two files, there's no shared test-support module to hang it on, and the repo's conventions beat my preferences — that's the standing rule, and a third spelling of json.replace is the local idiom, not new debt. Adding production API that only tests call would have been the worse trade, and you were right not to.
Correcting your own "twelve lines" to "roughly three hundred" before pushing is the same instinct that found the second file. Prose you wrote is still prose the next reader trusts.
📋 Non-blocking follow-ups
- Handler-level dispatch tests exist in exactly one file (
env_key_navigation.rs); thedocuments-absent fallback arm is untested there and at ~20 sibling sites, the oldest since May —main.rs:21168,main.rs:24626. Refuted as a blocker above, and it fails default-deny on materiality: not a latent hazard (degrades to a declined hover, never wrong data) and not debt with a testable "done" worth scheduling. Disposition: Noted — not tracked.
Ready for @mikebronner to merge.
`main` gained #353, which routes `assert_no_secret_leak` through `searchable()` — the strip that undoes `markdown_safety::escape_inline`, so a raw needle is still found in a field the panel escapes. This branch had added `URL_SECRET_TAIL` to that same needle list. Both belong; the merge takes the strip and the fifth needle together. The merge also stales a claim three lines below it. The canary `the_leak_search_still_finds_a_needle_the_panel_spells_with_escapes` pins `searchable` at its own definition — the helper is otherwise unexercised, because escaping can only hide a needle once a leak exists — and its doc said it did so "over the same four needles the helper searches". The helper now searches five, and the canary iterated four. Both swept: the canary takes `URL_SECRET_TAIL` as well, and the doc states the invariant rather than a count, since the count is exactly what goes stale when one list grows and the other does not. `URL_SECRET_TAIL` is a valid canary needle on its own terms — `tail-355` carries the ASCII punctuation `escape_inline` transforms, which is what the canary's first assertion requires of every row. Checked the sibling this merge could have blinded: `hover_masks_a_credential_ carried_inside_the_value` searches the raw needle in hover markdown. It is not blinded, because the value renders inside a fenced code block, which is verbatim — the positive assertion on the same markdown expects the unescaped masked URL and passes. Mutation-verified: `searchable` reduced to `json.to_string()` reddens the canary. Full suite 3454 pass, clippy and fmt clean. This merge is also what unblocks CI. The pull request had gone `CONFLICTING` against `main`, so GitHub could not build `refs/pull/358/merge` and dropped every `pull_request` event — two pushes in a row produced a CodeQL run and no CI, while other pull requests in the repo built normally throughout. Refs: #355
…ion. The comment above `languages` is the registry-facing answer to why a Laravel extension attaches to Shell Script (zed-industries/extensions#7370). It enumerated the callers of the `env_key_locator::is_env_file_name` gate, then asserted that every position request returns early unless the path ends in `.php` or `.blade.php`. #353 falsified that sentence. It gave `.env` buffers hover (`env_key_hover`) and go-to-definition (`env_key_definition`), both answering ahead of the `.php` gate and both classifying through `path_is_env_file`. It updated the matching passage in `docs/environment.md` and left the manifest behind — so the two justification sites disagreed, and the stale one is the file a registry reviewer opens first. The enumeration now names hover and go-to-definition. The false sentence is replaced by what is still true — find-references and rename remain `.php`/`.blade.php` only — followed by the load-bearing claim stated directly: a buffer Zed classifies as Shell Script but does not NAME `.env` fails the gate and reaches no env feature. Comment-only. No behaviour change.
Summary
Implements #341 — hover and go-to-definition now fire on a key declaration inside a
.env*buffer.Both handlers returned before any dispatch on such a buffer:
goto_definitiongated on.php,hoveron.blade.php/.php. Four other env features already classified files through the sharedenv_key_locator::is_env_file_namegate; these two now join them.Changes
hover— the key name, its effective value and declaring file (through the existing.env(2)/.env.local(1)/.env.example(0) ladderget_parsed_env_varalready applies), and its consumer count. Commented-out and not-defined states mirrorhover_for_env; the not-defined card keeps the count.goto_definition— jumps to the consumingenv('KEY')call sites. One hit returns aScalar, several return anArraycarrying every location. Zero consumers returnsNone, where hover still renders a card.get_patternsPHP-pattern lookup, so admission reaches a real result instead of falling through intoOk(None).env_key_locator::enumerate_commented_keys_in_source— finds a key on a#line, whichenumerate_keys_in_sourceclassifies as "not a declaration". Both delegate to the sameparse_key_declaration; they stay separate so a commented# KEY=cannot win the first-match-wins race and move the env code lens onto a comment.code_lens::reference_count_label+Backend::reference_locations— the count phrasing and the de-duplicated location lookup were inline incode_lens_resolve. Extracted, with both call sites routed through them, so a key's lens count and its hover count come from one call and cannot disagree..envtext has no charset restriction, and both the hover card and the completion panel areMarkupKind::Markdown. The two renderers escape the bold header and negotiate the code fence for every caller;CompletionDoc::summaryis markdown-bearing by contract (a PHPDoc summary carries real markdown), so the.envbranch ofcompletionescapes the value it puts there itself. Without it,KEY=[text](https://evil.example)renders a live link in the completion panel andKEY=is fetched with no click.environment.mdstated hover and go-to-definition "only ever run on.phpand.blade.php", which this change falsifies. Corrected, with its surrounding argument (a real.shgets nothing) intact.hover.md,go-to-definition.mdandcomparison.mddocument the new surface.Two AC premises corrected
Both are stale rather than wrong-headed; flagging them rather than silently diverging.
is_env_file_nameexists. The AC's grounding comment reported it absent fromenv_key_locator.rsand pointed the work at the inline spellingfile_name == ".env" || file_name.starts_with(".env."). PR Route every "is this an env file?" check through one strict predicate (3 spellings, 2 loose) #346 has since landed and routed every env-classification site throughis_env_file_name, whose body is that expression. Calling it is therefore the AC's exact predicate with no second spelling to drift — the stronger reading of the same requirement.get_parsed_env_var. The AC directs commented-key detection atparse_env_source/ParsedEnvVarposition data "reached throughget_parsed_env_var", but that call is keyed by name and so cannot answer "which key is at this cursor". Its only positional sibling,get_all_parsed_env_vars, merges by name across files by priority, which drops exactly the case the AC asks about — a commented declaration in a file outranked by another. Detection reads the buffer instead (the authoritative source for "in this file"), through the sameparse_key_declarationthe rest of the module uses. Value, source file and commented state still come fromget_parsed_env_var, as the AC requires.Acceptance Criteria
goto_definitionandhoverusing the shared strict predicate (viapath_is_env_file→is_env_file_name), not a new spelling and not a loosecontains/prefix form; admission branches to a dedicated env path ahead of the PHP-pattern lookup.enumerate_keys_in_source; cursor on the=, the value, or a blank line resolves nothing; a same-key re-declaration resolves nothing (first-match-wins).parse_key_declaration— see premise 2 above. Go-to-definition treats them identically to active keys..env/.env.local/.env.exampleladder) and consumer count incode_lens_resolve's exact phrasing — now literally the same function.0 references.*(not defined in .env)* — N references, keeping the count.Scalarfor one consumer,Arrayof every consumer for more — never truncated.Ok(None)from go-to-definition while hover still renders; the two diverge by design.textDocument/referenceskeeps its.php/.blade.phpgate — out of scope, pinned by a test..envrc,.environmentandmy.env.localstill returnOk(None)from both handlers, proven by a fixture carrying a valid declaration so only the gate can turn it away.HoverParams/GotoDefinitionParamsagainst a.env/.env.exampleURI and drivinghover/goto_definitionthrough atower_lsp::LspServiceharness.Note on the last one: the AC cites an "existing
.php/.blade.phpdispatch test convention" for driving these two handlers. No such test existed — nothing in the repo calledhover/goto_definitionat the trait level before this PR. The requirement is met by building that harness here; it is new, not copied.Test Plan
25 handler-level tests (
src/tests/env_key_navigation.rs) + 7 locator unit tests (src/env_key_locator/tests.rs) + 11 unit tests for the two escaping primitives (src/markdown_safety/tests.rs).Full suite green: 2704 lib + 665 bin + 80 integration + 2 doctests, 0 failures (
cargo test --all-features, as CI runs it). Counts rose from 2703/651 with themainmerge, which brought Cache-hit regression tests are blind to config_lookup::resolve_value's disk reads #357's added tests, and from 663 bin with round 6's two added fixtures.CI green on all four legs — ubuntu, macos, windows, and the wasm extension check.
cargo clippy --all-targets -- -D warningsclean,cargo fmt --checkclean.Mutation-verified — 29 applied live across five rounds, 29 killed. Rounds 1-3 are recorded in the review thread (9, 10 and 3). Round 4 applied the six below, each reverted after its run:
hover::render— the markdown-link fixture reddens.hover::render's plain arm — both the.env-buffer fence test and the reverse-direction one redden.!exempted, i.e. a hand-listed set instead of the spec's) — the image fixture and the completeness test both redden. This is the mutation that argues for delegating the set tochar::is_ascii_punctuation.CompletionDoc::render— threeenv_value_redactionpanels redden, which is how the third site proved it was live.hover_for_env_declarationlinking the queriedpathinstead ofvar.source_file— the lower-priority ladder test reddens, the hole the old comment described but the body did not assert.contains— its own test reddens on"10 references"and"1 references".The equal-priority arm's fourth row (both declarations commented) stays deliberately unpinned, for the reason given last round: the commented branch is cursor-derived and renders no value, so no output distinguishes the two.
Round 5 applied one more:
.envcompletion branch — 13 tests redden, includinga_value_spelling_a_markdown_link_renders_inert_in_the_panel, which was written red before the fix and asserts against literal escaped text rather than through the sharedexpected_panelhelper.Round 5 declared the backslash strip in
assert_no_process_var_leaknot mutation-covered, on the grounds thatlabelanddetailstill catch a restoredstd::env::vars()loop. That was honest but incomplete: the strip is discriminating, against a summary-only leak rather than a wholesale one. Round 6 applied three more mutations, all killed, which is what retires that caveat:.envcompletion summary made to answer from the real value,detailleft redacted — a summary-only leak.env_value_redaction'sassert_no_secret_leaknow reddens at it; before round 6 it returned clean and the test failed later on panel equality.std::env::var— a summary-only leak of the process value, which the first mutation does not produce.env_completion_system_leak's shadowing check now reddens at it, having previously passed it through.searchable()neutered to return its input unchanged, in both files at once. Exactly the two new fixtures redden and nothing else, so each pins its own file's strip alone.Running total: 32 mutations applied live across six rounds, 32 killed.
Rendering change, and what it does not change
Escaping alters the wire text of every card and panel header containing punctuation (
APP\_NAME,App\\Models\\User), and of the.envvalue in the completion panel, which is why 33 assertions across nine test files moved to the escaped form and two panel helpers followed in round 5. It does not alter what anyone sees: CommonMark renders\.as.. No user-facing documentation changes, because there is no user-facing behaviour change — unlike the redaction work in #344/#348, which hid values.Fixes #341