diagnostics: point inside quoted arguments, share the locating - #13934
Conversation
|
GNU testsuite comparison: |
Merging this PR will improve performance by 7.07%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR improves caret-style diagnostics rendering so underline/caret positions remain accurate even when arguments are echoed back with quoting, and it centralizes two previously duplicated diagnostic behaviors (the “expression already complete” label and option-value locating/rendering).
Changes:
- Update snapshot locating so offsets inside an operand still map correctly when the argument is quoted (e.g., contains spaces), and refine fallback behavior at operand boundaries.
- Introduce shared boundary/character-span helpers and a shared
Snapshot::render_option_value(...)helper to reduce per-utility duplication. - Consolidate the “expression was already complete here” diagnostic label into uucore locales and reuse it from
testandexpr.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/by-util/test_printf.rs | Updates the expected caret column/underline for a quoted format-string argument. |
| src/uucore/src/lib/features/mode.rs | Switches mode diagnostics rendering to the shared option-value renderer and factors span/label/help derivation. |
| src/uucore/src/lib/features/diagnostics.rs | Implements shared render_option_value, improves locating within quoted args, and re-exports boundary helpers. |
| src/uucore/src/lib/features/diagnostics_stub.rs | Mirrors the new API (re-exports boundary helpers and adds render_option_value) for builds without the diagnostics feature. |
| src/uucore/src/lib/features/diagnostics_boundary.rs | New shared module for character-boundary flooring and single-character span calculation (with unit tests). |
| src/uucore/src/lib/features.rs | Adds the shared diagnostics_boundary module used by both real and stub diagnostics implementations. |
| src/uucore/locales/en-US.ftl | Adds shared diagnostics label diagnostics-label-expression-complete. |
| src/uucore/locales/fr-FR.ftl | Adds shared diagnostics label diagnostics-label-expression-complete. |
| src/uu/test/src/diagnostics.rs | Reuses the shared “expression complete” label key from uucore. |
| src/uu/test/locales/en-US.ftl | Removes the now-duplicated per-utility label key. |
| src/uu/test/locales/fr-FR.ftl | Removes the now-duplicated per-utility label key. |
| src/uu/expr/src/diagnostics.rs | Reuses the shared “expression complete” label key from uucore. |
| src/uu/expr/locales/en-US.ftl | Removes the now-duplicated per-utility label key. |
| src/uu/expr/locales/fr-FR.ftl | Removes the now-duplicated per-utility label key. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
91f53da to
e5d6db4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/uucore/src/lib/features/diagnostics.rs:436
- The docstring states that an empty
rangemarks the character it starts at, but the implementation now falls back to underlining the whole operand when the empty range is at the end (seea_range_at_the_end_of_an_operand_falls_back_to_the_operand). Please update the docs to describe the end-of-operand behavior (or adjust behavior to match the docs) so callers have an accurate contract.
/// * `range` - Byte range inside `operand` to point at. An empty range
/// marks the character it starts at.
src/uucore/locales/en-US.ftl:115
- A new shared localization key is introduced in uucore and is now referenced by multiple utilities, but it’s only added for en-US and fr-FR in this PR. If the project ships additional uucore locale files, those locales will lack this key and may produce fallback/blank strings or missing-key warnings at runtime. Consider adding this key to the remaining uucore locales (even if temporarily in English) to keep localization behavior consistent.
# Diagnostic label shared by the utilities whose arguments are an expression
diagnostics-label-expression-complete = the expression was already complete here
src/uucore/src/lib/features/diagnostics_boundary.rs:30
floor_boundarycurrently scans fromoffsetdown to 0 using an iterator, which is O(n) with some iterator overhead per call. Since diagnostics may call this repeatedly, consider using a simple decrementing loop (e.g., start atoffset.min(text.len()), decrement untilis_char_boundary) to reduce overhead while keeping the same semantics.
pub fn floor_boundary(text: &str, offset: usize) -> usize {
let offset = offset.min(text.len());
(0..=offset)
.rev()
.find(|&i| text.is_char_boundary(i))
.unwrap_or(0)
}
e5d6db4 to
09e2239
Compare
09e2239 to
ccda6d1
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/uucore/src/lib/features/diagnostics.rs:436
- The docs for
rangeno longer match the updated behavior inlocate_operand: an empty range at the end of the operand falls back to underlining the whole operand (per the updated tests), not “the character it starts at” (there is no character). Update this doc comment to describe the end-of-operand behavior explicitly (e.g., “empty range marks the character it starts at; if it starts at/after the end of the operand, the whole operand is underlined”).
/// * `range` - Byte range inside `operand` to point at. An empty range
/// marks the character it starts at.
src/uucore/src/lib/features/mode.rs:92
self.describe()performs translations unconditionally, even thoughrender_option_valuecan returnfalseearly whenindex_of_valuecan’t find the option value. Previously, that translation work happened only after finding the value. Consider making translation lazy here (e.g., first find the index/value location, then translate and callrender_inside_at), or adjustingrender_option_valueto accept label/help as deferred computation so translations only happen when rendering proceeds.
let (label, help) = self.describe();
crate::diagnostics::Snapshot::new(args).render_option_value(
mode,
Some('m'),
Some("mode"),
self.clause_span(clause_start),
message,
label.as_deref(),
help.as_deref(),
)
expr and test both point a caret at an argument that came after the expression was done, and both said the same thing about it, word for word, from a key of their own. Two keys holding one sentence drift apart the moment either is translated again, and these two utilities are the pair a reader most expects to behave alike. Move the sentence to uucore, next to the other strings a caret report is built from. A utility's bundle already carries uucore's, so both resolve it as they resolved their own.
An argument holding a space is echoed back quoted, and the caret then gave up on it: offsets into the operand no longer matched the text that was printed, so the whole argument was underlined instead. That is the common case for anything whose operand is a command line rather than a word — `printf 'hello %s and %z'` pointed at all of it. Quoting only wraps the operand, though, so its bytes are still there, contiguous, a little further along. Look for them and count from where they turned up rather than from the end of the argument. An argument that had to be escaped to be printed — a non-UTF-8 one, or one whose quotes had to be broken up — does not contain them contiguously, and keeps the plain underline it has now. This also retires the `verbatim` flag, which only ever answered the question the search now answers properly.
Every operand that is the value of an option needs the same two steps before a caret can be drawn: find the argument carrying it, then point inside it. `mode.rs` spelled that out for `-m`, and sort and numfmt spell it out again for `-k` and `--format`; cut and the size parsers are about to want it too. Move the pair onto `Snapshot`, and expose the two boundary helpers the module already used internally — an offset handed in by someone else's parser has to be walked back to a character boundary before it can be sliced with, and each caller was about to grow its own copy.
ccda6d1 to
cc947ba
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/uucore/src/lib/features/mode.rs:92
- ModeError::render
now translateslabel/helpviadescribe()even whenrender_option_valuewill immediately returnfalse(e.g., diagnostics compiled out via the stub, or when the option/value isn’t found). Previously this path could avoid translation work by checkingindex_of_valuefirst. Consider restructuring to only translate after confirming the value is present (e.g., explicitly dolet snap = Snapshot::new(args); let Some(index) = snap.index_of_value(...) else { return false; };then callrender_inside_at, or introduce a variant ofrender_option_value` that takes untranslated keys / a lazy translation callback).
let (label, help) = self.describe();
crate::diagnostics::Snapshot::new(args).render_option_value(
mode,
Some('m'),
Some("mode"),
self.clause_span(clause_start),
message,
label.as_deref(),
help.as_deref(),
)
src/uucore/src/lib/features/diagnostics.rs:436
- The doc for
rangesays an empty range “marks the character it starts at”, but the current behavior falls back to underlining the whole operand when the empty range starts at the end of the operand (seea_range_at_the_end_of_an_operand_falls_back_to_the_operand). Please update the doc comment to reflect the end-of-operand case (e.g., “marks the character it starts at, or the whole operand if it starts at the end”).
/// * `range` - Byte range inside `operand` to point at. An empty range
/// marks the character it starts at.
Three pieces of sharing. An argument holding a space is echoed quoted, so an
offset inside it no longer lands where the operand ends — the caret now follows
the operand's bytes wherever the quoting put them. The
expression was already completelabel and the locating of an option's value both move to one placeinstead of being spelled out per utility.
Stack created with GitHub Stacks CLI • Give Feedback 💬