Xml: `positions` decode option that puts `line` and `column` on `StartElement` events
I maintainer di solito rispondono entro 1 giorno
Valutazione
Questa issue non è ancora stata valutata.
Descrizione
What is the problem this feature would solve?
Xml.decodeString and Xml.decode emit events without a line or column, so a consumer that rejects a decoded value cannot report where in the input that value was. Only an XmlError carries a position, and a document the decoder accepts never raises one.
Ran against effect 4.1.2 (506eef1) on Bun 1.4.2.
Decoding a synthetic document and reading the events:
import { Effect, Stream } from "effect"
import { Xml } from "effect/encoding"
const xml = `<order id="7">
<item sku="a1"><price>12.50</price></item>
<item sku="b2"><price>twelve</price></item>
</order>`
const events = await Effect.runPromise(
Stream.make(xml).pipe(Stream.pipeThroughChannel(Xml.decodeString()), Stream.runCollect)
)
const starts = events.filter((e) => e._tag === "StartElement")
console.log("StartElement keys:", Object.keys(starts[0]))
const priceText = events[events.findLastIndex((e) => e._tag === "StartElement" && e.name === "price") + 1]
console.log("price text:", JSON.stringify(priceText), "-> Number:", Number((priceText as Xml.Text).text))
StartElement keys: [ "_tag", "name", "attributes" ]
price text: {"_tag":"Text","text":"twelve"} -> Number: NaN
Control. A syntax error in the same shape of document is positioned: decoding <order>\n <item sku="a1">\n</order> fails with XmlError Syntax, line 3, column 1, the < of </order>, message end tag </order> does not match <item> at line 3, column 1.
This is the documented contract. line and column belong to XmlError, and decode and decodeString set them on errors:
https://github.com/Effect-TS/effect/blob/506eef143bcc1750ce58920aac1e45da7265d103/packages/effect/src/encoding/Xml.ts#L43-L45
StartElement has _tag, name and attributes:
https://github.com/Effect-TS/effect/blob/506eef143bcc1750ce58920aac1e45da7265d103/packages/effect/src/encoding/Xml.ts#L133-L137
The parser already tracks the line and column of the document offset it has counted to, and uses them to position errors:
https://github.com/Effect-TS/effect/blob/506eef143bcc1750ce58920aac1e45da7265d103/packages/effect/src/encoding/Xml.ts#L1037-L1045
Use case (synthetic). A consumer reads each <item> into a typed record: sku from the start tag's attributes, price from the Text event under <price> through Schema.NumberFromString. When "twelve" is rejected, the consumer can name the element path (order/item[2]/price) but not line 3, column 18, where the <price> start tag is, because nothing in the events says where it was. The path names the value in the decoded model; a line and column name it in the file its author edits, which is what a validation report or an editor jump points at, and the parser is the only component that holds an event's document offset at the moment it emits the event. For a document streamed through Xml.decode from a file or a response body, the text is no longer available to search, so the position is lost for good.
What is the feature you are proposing to solve the problem?
Request: a positions option on DecodeOptions. When it is true, decode and decodeString set line and column on every StartElement event, at the < of the start tag, the offset at which the parser already positions the tag's own Limit errors. The start tag is the anchor because every element has one, and because a text node is one token that can hold references and CDATA sections, so a single position on Text would not name its characters. Both fields are one-based and follow the rules XmlError already documents: columns count UTF-16 code units, lines end at LF, CR or CRLF, and a leading byte order mark is not counted.
Markup is parsed from at most maxTokenSize + 1 characters and a start tag's Limit checks run before it is pushed, so maxTokenSize and maxDepth are enforced before a positioned event exists, as today:
https://github.com/Effect-TS/effect/blob/506eef143bcc1750ce58920aac1e45da7265d103/packages/effect/src/encoding/Xml.ts#L1156-L1159
https://github.com/Effect-TS/effect/blob/506eef143bcc1750ce58920aac1e45da7265d103/packages/effect/src/encoding/Xml.ts#L1365-L1371
The fields are declared optional on StartElement and are absent when the option is not set. StartElement events are object literals with exactly _tag, name and attributes; without the option the same literal is pushed, so the keys are absent rather than set to undefined, and existing events are unchanged. The encoders and Xml.elements read name and attributes by field and the module never spreads an event, so they are unchanged, and a consumer that constructs StartElement values for encode needs no new fields:
https://github.com/Effect-TS/effect/blob/506eef143bcc1750ce58920aac1e45da7265d103/packages/effect/src/encoding/Xml.ts#L1741
https://github.com/Effect-TS/effect/blob/506eef143bcc1750ce58920aac1e45da7265d103/packages/effect/src/encoding/Xml.ts#L1937
Always-present fields would change the shape of every StartElement the default path emits, so the fields are opt-in. The option changes which fields StartElement carries and nothing else: the same events are pushed at the same points, and the position is read from the line, lineStart and counted fields the parser already keeps for XmlError, so no input is retained beyond what parse holds today:
https://github.com/Effect-TS/effect/blob/506eef143bcc1750ce58920aac1e45da7265d103/packages/effect/src/encoding/Xml.ts#L967-L970
On the documented side the change is one bullet in the DecodeOptions doc block and two optional fields on StartElement.
export interface DecodeOptions {
readonly maxTokenSize?: number | undefined
readonly maxDepth?: number | undefined
readonly positions?: boolean | undefined
}
export interface StartElement {
readonly _tag: "StartElement"
readonly name: string
readonly attributes: Readonly<Record<string, string>>
readonly line?: number | undefined
readonly column?: number | undefined
}
Not requested. Positions on Text, EndElement, Comment, ProcessingInstruction, Declaration or Doctype; end positions or byte offsets; positions on Element from Xml.elements; any change to the encoders, to XmlError or to the default output of decode and decodeString.
Feasibility. Tested in a scratch copy at 506eef1: the acceptance check passes, the existing Xml.test.ts cases stay green (21 of 21), the default path pushes the same literal as before behind one added boolean test per start tag and measured within noise of the unpatched copy (0.81x to 1.29x over three runs), and with positions: true a synthetic 400 KB document decoded in 4096-character chunks took 1.5x the default time (median of 10 alternating rounds), the scratch copy reading each position with the error path's count, which advances counted forward only and is advanced to where parsing stopped at the end of every chunk, so each character is counted once per decode and no chunk is rescanned.
Not confirmed.
- Whether leaving events without positions was a deliberate choice in #9420. Its description and review comments do not say.
- Whether the fields should be typed as present under
positions: true, through an overload ofdecodeanddecodeStringor a distinct positioned event type, rather than as optional fields onStartElement. Optional fields are the smaller change; a consumer that set the option narrows withe.line !== undefined.
Acceptance check. A test in packages/effect/test/encoding/Xml.test.ts that fails at 506eef1 with Property 'line' does not exist on type 'StartElement' and passes after the change:
it.effect("start tags carry line and column when positions is true, however the input is chunked", () =>
Effect.gen(function*() {
// synthetic
const xml = "<?xml version=\"1.0\"?>\r\n<!-- c -->\n<order id=\"7\">\r <item sku=\"a1\"><price>12.50</price></item>\n <item sku=\"b2\"><price>twelve</price></item>\n</order>"
const expected = [["order", 3, 1], ["item", 4, 3], ["price", 4, 18], ["item", 5, 3], ["price", 5, 18]]
for (const chunks of splits(xml)) {
const events = yield* decodeString(chunks, { positions: true })
const positions = events.flatMap((e) => e._tag === "StartElement" ? [[e.name, e.line, e.column]] : [])
assert.deepStrictEqual(positions, expected, JSON.stringify(chunks))
}
const plain = yield* decodeString([xml])
for (const event of plain) {
assert.isFalse("line" in event)
assert.isFalse("column" in event)
}
const positioned = yield* decodeString([xml], { positions: true })
assert.deepStrictEqual(
positioned.map((e) => e._tag === "StartElement" ? { _tag: e._tag, name: e.name, attributes: e.attributes } : e),
plain
)
}))
decodeString and splits are the helpers that file already defines; splits yields the input one character per chunk and at every two-way split, so a tag split in its name and a CR split from its LF are covered.
What alternatives have you considered?
XmlError.lineandXmlError.column: set only when decoding fails. A document the decoder accepts but that holds a value the consumer rejects decodes without anXmlError, so there is no position to read.Xml.elements:Elementcarriesname,attributesandchildrenand no position, and it drops the chunk structure, so the position cannot be recovered from the outside either.- Counting lines outside the decoder, using the contract that each output chunk holds the events completed by one input chunk: re-chunk the input at every
<, pull the events for each piece, and attribute them to the line count at that piece. Measured against plaindecodeStringon Bun 1.4.2: 2.48x and 2.89x the time on a 4,888-character document fed as one chunk and in 4096-character chunks, and 3.56x on a synthetic 437 KB document. Feeding the same pieces to plaindecodeStringwith no counting at all already costs 3.00x, so about three quarters of the cost is the per-piece pull round trip through the channel, which cannot be avoided from outside the channel: one piece per<is the only way to attribute an event to its input. - Locating the position after the fact: keep the decoded text, walk the element path back to the start tag with a second scanner, and read its line and column. Serves
decodeStringover a string already in memory and costs nothing until a value is rejected. It needs every chunk retained until its record is accepted, so it does not serveXml.decodeover a byte stream, and the second scanner has to agree with the parser on every edge of the grammar (comments, CDATA sections, references, attribute quoting) to land on the same<.
Related. #9420, which added the module with line and column on XmlError.
AI disclosure: Written by AI and reviewed by the filer before filing.
- Provider: Anthropic
- Model: Fable 5.1
- Lingua principale
- TypeScript
- Stelle
- 16.7k
- Fork
- 808
- Merge medio
- 11h 40m
- PR unite (30g)
- 827
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di Effect-TS/effect
-
sql-pg: pgvector columns fail with "Invalid UTF-8 in text value"Forse già presa @effect-bot l’ha presa oggi. Apertabug
Effect-TS/effect#10420 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
cli: completions break for subcommand names with `:`Forse già presa @effect-bot l’ha presa 1 giorno fa. Aperta
Effect-TS/effect#10416 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
McpServer.layerStdio: native EOF cancellation policy for active tool handlersForse già presa @effect-bot l’ha presa 1 giorno fa. Aperta
Effect-TS/effect#10412 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
add OAuth authorization support to HTTP McpServerForse già presa @effect-bot l’ha presa 1 giorno fa. Apertaenhancement
Effect-TS/effect#10402 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
effect/ai: support the MCP Apps extension (`io.modelcontextprotocol/ui`)Forse già presa @effect-bot l’ha presa 1 giorno fa. Aperta
Effect-TS/effect#10399 · 2 commenti · 1 reazione · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di Effect-TS/effect
Issue simili
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
Yeraze/meshmonitor#6239 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
better-auth/better-auth#12292 · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
The sample email preview's ResizeObserver is never disconnected and keeps raising errors after leaving SettingsForse già presa Una pull request collegata a questa issue è aperta o già unita. Apertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
-
area:ui bug effort:S priority:P2
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
Typo in WSL project description on Projects pageForse già presa @akhil9tiet l’ha presa oggi. Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 75/100
microsoft/opensource.microsoft.com#1066 · 1 commento ·