Skip to content

cue/errors: error deduplication removes distinct errors at same position #4270

@mpvl

Description

@mpvl

Summary

The approximateEqual function in error deduplication considers errors equal if they have the same position and path, ignoring message content. While this behavior may be rare in practice with pure CUE code, it can hide distinct errors when they occur.

Current Behavior

errors.Print() calls sanitize() which uses removeMultiples() to deduplicate errors:

// removeMultiples sorts a list and removes all but the first error per line.
func (p *list) removeMultiples() {
    p.sort()
    *p = slices.CompactFunc(*p, approximateEqual)
}

func approximateEqual(a, b Error) bool {
    aPos := a.Position()
    bPos := b.Position()
    if !aPos.IsValid() || !bPos.IsValid() {
        return a.Error() == b.Error()  // Falls back to message comparison
    }
    return aPos.Compare(bPos) == 0 && slices.Compare(a.Path(), b.Path()) == 0
    // Note: message content is NOT compared when positions are valid
}

The comment says "removes all but the first error per line" but the implementation removes errors with the same position+path regardless of whether the messages are different.

Problem

When two distinct errors occur at the same position with the same path, only one is shown. This can happen when:

  1. Multiple validation phases generate different errors for the same AST node
  2. Tool integrations or custom code appends errors to an existing error list
  3. Future language features add new error types at existing positions

Reproducible Example

// Create two distinct errors at same valid position with same path
f := token.NewFile("test.cue", 1, 100)
f.SetLinesForContent([]byte("line1\nline2\nline3\n"))
pos := f.Pos(10, token.NoRelPos)

e1 := &testError{pos: pos, msg: "first error message", path: []string{"field"}}
e2 := &testError{pos: pos, msg: "second error message", path: []string{"field"}}

combined := errors.Append(nil, e1)
combined = errors.Append(combined, e2)

// Before sanitization: both errors present
errors.Errors(combined)
// [0]: first error message
// [1]: second error message

// After errors.Print(): only one shown
errors.Print(&buf, combined, nil)
// field: first error message:
//     test.cue:2:5
//
// "second error message" is hidden

Observations

In practice with pure CUE code, this situation appears to be rare - CUE's compiler and evaluator generally avoid generating distinct errors at the exact same position+path. However:

  1. The code comment ("per line") doesn't match the implementation (per position+path)
  2. Duplicate identical errors ARE correctly deduplicated (e.g., let errors)
  3. The behavior could cause issues for tooling that combines error lists

Suggested Fix

Change approximateEqual to also compare messages when positions are valid:

func approximateEqual(a, b Error) bool {
    aPos := a.Position()
    bPos := b.Position()
    if !aPos.IsValid() || !bPos.IsValid() {
        return a.Error() == b.Error()
    }
    return aPos.Compare(bPos) == 0 &&
           slices.Compare(a.Path(), b.Path()) == 0 &&
           a.Error() == b.Error()  // Also compare messages
}

This would only deduplicate truly duplicate errors while preserving distinct ones.

We would need to verify the performance impact of this change.

Related

Found while investigating try expression error handling (issue #4019).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions