report

package
v0.14.2-0...-12ef1ef Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 29, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package report provides a robust diagnostics framework. It offers diagnostic construction, interchange, and ASCII art rendering functionality.

Diagnostics are collected into a Report, which is a helpful builder over a slice of [Diagnostic]s. Each Diagnostic consists of a Go error plus metadata for rendering, such as source code spans, notes, and suggestions. This package takes after Rust's diagnostic philosophy: diagnostics should be pleasant to read, provide rich information about the error, and come in a standard, machine-readable format.

Reports can be rendered using a Renderer, which provides several options for how to render the result to the user.

A Report can be converted into a Protobuf using Report.ToProto. This can be serialized to e.g. JSON as an alternative error output.

The [File] type is a generic utility for converting file offsets into text editor coordinates. E.g., given a byte offset, what is the user-visible line and column number? Package report expects the caller to construct this information themselves, to avoid recomputing it unnecessarily.

Defining Diagnostics

Generally, to define a diagnostic, you should define a new Go error type, and then make it implement Diagnose. This has two benefits:

  1. When someone using your tool as a library looks through a Report, they can type assert Diagnostic.Err to programmatically determine the nature of a diagnostic.

  2. When emitting the diagnostic in different places you get the same UX. This means you should do this even if the error type will be unexported.

Sometimes, (2) is not enough of a benefit, in which case you can just use Report.Errorf() and friends.

Diagnostics Style Guide

Diagnostics created with package report expect to be written in a certain way. The following guidelines are taken, mostly verbatim, from the Rust Project's diagnostics style guide.

The golden rule: Users will see diagnostics when they are frustrated. Do not make them more frustrated. Do not make them feel like your tool does not respect their intelligence.

  1. Errors are for semantic constraint violations, i.e., the compiler will not produce valid output. Warnings are for when the compiler notices something not strictly forbidden but probably bad. Remarks are essentially warnings that are not shown to the user by default. Diagnostic notes are for factual information that adds context to why the diagnostic was shown. Diagnostic help is for prose suggestions to the user. Diagnostic debugs are never shown to normal users, and are for compiler debugging only.

  2. Diagnostics should be written in plain, friendly English. Your message will appear on many surfaces, such as terminals and LSP plugin insets. The golden standard is that the error message should be readable and understandable by an inexperienced, hung-over programmer whose native language is not European, displayed on a dirty budget smartphone screen.

  3. Diagnostic messages do not begin with a capital letter and do not end in punctuation. The compiler does not ask questions. The words "error", "warning", "remark", "help", and "note" are NEVER capitalized. Never refer to "a diagnostic"; prefer something more specific, like "compiler error".

  4. Error messages should be succinct: short and sweet, keeping in mind (1). Users will see these messages many, many times.

  5. The word "illegal" is illegal. We use this term inside the compiler, but the word may have negative connotations for some people. "Forbidden" is also forbidden. Prefer "invalid", "not allowed", etc.

  6. The first span in a diagnostic (the primary span) should be precisely the code that resulted in the error. Try to avoid more than three spans in an error. Try to pick the smallest spans you can: instead of highlighting a whole type definition, try highlighting just its name.

  7. Try not to emit multiple diagnostics for the same error. This requires more work in the compiler, but it is worth it for the UX.

  8. If your tool does not have enough information to emit a good diagnostic, that is a bug in either your tool, or in the language your tool operates on (in both cases, it is the tool's job to acquire this information).

  9. When talking about your tool, call it "the compiler", "the linter", etc. Your tool is a machine, not a person; therefore it does not speak in first person. When referring to a programming language's semantics, rather than the compiler's, use that language's name. For example, "Go does not support...", "... is not valid Protobuf", "this is a limitation of C++".

Index

Constants

This section is empty.

Variables

View Source
var PageBreak pageBreak

PageBreak is a DiagnosticOption that inserts a "page break", separating diagnostic snippets before and after it into separate windows.

Functions

This section is empty.

Types

type Diagnose

type Diagnose interface {
	Diagnose(*Diagnostic)
}

Diagnose is a type that can be rendered as a diagnostic.

type Diagnostic

type Diagnostic struct {
	// contains filtered or unexported fields
}

Diagnostic is a type of error that can be rendered as a rich diagnostic.

Not all Diagnostics are "errors", even though Diagnostic does embed error; some represent warnings, or perhaps debugging remarks.

To construct a diagnostic, create one using a function like Report.Error. Then, call Diagnostic.Apply to apply options to it. You should at minimum apply Message and either InFile or at least one Snippetf.

func (*Diagnostic) Apply

func (d *Diagnostic) Apply(options ...DiagnosticOption) *Diagnostic

Apply applies the given options to this diagnostic.

Nil values are ignored; does nothing if d is nil.

func (*Diagnostic) Debug

func (d *Diagnostic) Debug() []string

Debug returns this diagnostic's debugging information, set using Debugf.

func (*Diagnostic) File

func (d *Diagnostic) File() string

File returns the path of the file this diagnostic is associated with.

It returns the value set by InFile if present, otherwise it returns the path from the primary span. Returns empty string if neither is available.

func (*Diagnostic) Help

func (d *Diagnostic) Help() []string

Help returns this diagnostic's suggestions, set using Helpf.

func (*Diagnostic) Is

func (d *Diagnostic) Is(tag string) bool

Is checks whether this diagnostic has a particular tag.

func (*Diagnostic) Level

func (d *Diagnostic) Level() Level

Level returns this diagnostic's level.

func (*Diagnostic) Message

func (d *Diagnostic) Message() string

Message returns this diagnostic's message, set using Message.

func (*Diagnostic) Notes

func (d *Diagnostic) Notes() []string

Notes returns this diagnostic's notes, set using Notef.

func (*Diagnostic) Primary

func (d *Diagnostic) Primary() source.Span

Primary returns this diagnostic's primary span, if it has one.

If it doesn't have one, it returns the zero span.

func (*Diagnostic) Tag

func (d *Diagnostic) Tag() string

Tag returns this diagnostic's tag, set using Tag.

type DiagnosticOption

type DiagnosticOption interface {
	// contains filtered or unexported methods
}

DiagnosticOption is an option that can be applied to a Diagnostic.

IsZero values passed to Diagnostic.Apply are ignored.

func Debugf

func Debugf(format string, args ...any) DiagnosticOption

Debugf returns a DiagnosticOption appends debugging information to a diagnostic that is not intended to be shown to normal users.

func Helpf

func Helpf(format string, args ...any) DiagnosticOption

Helpf returns a DiagnosticOption that provides the user with a helpful prose suggestion for resolving the diagnostic.

func InFile

func InFile(path string) DiagnosticOption

InFile returns a DiagnosticOption that causes a diagnostic without a primary span to mention the given file.

func Message

func Message(format string, args ...any) DiagnosticOption

Message returns a DiagnosticOption that sets the main diagnostic message.

func Notef

func Notef(format string, args ...any) DiagnosticOption

Notef returns a DiagnosticOption that provides the user with context about the diagnostic, after the annotations.

func Snippet

func Snippet(at source.Spanner) DiagnosticOption

Snippet is like Snippetf, but it attaches no message to the snippet.

The first annotation added is the "primary" annotation, and will be rendered differently from the others.

If at is nil or returns the zero span, the returned DiagnosticOption is a no-op.

func Snippetf

func Snippetf(at source.Spanner, format string, args ...any) DiagnosticOption

Snippetf returns a DiagnosticOption that adds a new snippet to a diagnostic.

Any additional arguments to this function are passed to fmt.Sprintf to produce a message to go with the span.

The first annotation added is the "primary" annotation, and will be rendered differently from the others.

If at is nil or returns the zero span, the returned DiagnosticOption is a no-op.

func SuggestEdits

func SuggestEdits(at source.Spanner, message string, edits ...Edit) DiagnosticOption

SuggestEdits is like Snippet, but generates a snippet that contains machine-applicable suggestions.

A snippet with suggestions will be displayed separately from other snippets. The message associated with the snippet will be prefixed with "help:" when rendered.

func SuggestEditsWithWidening

func SuggestEditsWithWidening(at source.Spanner, message string, edits ...Edit) DiagnosticOption

SuggestEditsWithWidening is like SuggestEdits, but it allows edits' starts and ends to not conform to the given span exactly (e.g., the end points are negative or greater than the length of the span).

This will widen the span for the suggestion to fit the edits.

func Tag

func Tag(t string) DiagnosticOption

Tag returns a DiagnosticOption that sets a diagnostic's tag.

Tags are machine-readable identifiers for diagnostics. Tags should be lowercase identifiers separated by dashes, e.g. my-error-tag. If a package generates diagnostics with tags, it should expose those tags as constants.

type Edit

type Edit struct {
	// The start and end offsets of the edit, relative the span of the snippet
	// this edit is applied to (so, Start == 0 means the edit starts at the
	// start of the span).
	//
	// An insertion without deletion is modeled by Start == End.
	Start, End int

	// Text to replace the content between Start and End with.
	//
	// A pure deletion is modeled by Replace == "".
	Replace string
}

Edit is an edit to suggest on a snippet.

See SuggestEdits.

func (Edit) IsDeletion

func (e Edit) IsDeletion() bool

IsDeletion returns whether this edit involves deleting part of the source text.

func (Edit) IsInsertion

func (e Edit) IsInsertion() bool

IsInsertion returns whether this edit involves inserting new text.

type Level

type Level int8

Level represents the severity of a diagnostic message.

const (
	// Internal compiler error. Indicates a panic within the compiler.
	ICE Level = 1 + iota
	// Red. Indicates a semantic constraint violation.
	Error
	// Yellow. Indicates something that probably should not be ignored.
	Warning
	// Cyan. This is the diagnostics version of "info".
	Remark
)

type Options

type Options struct {
	// The stage to apply to any new diagnostics created with this report.
	//
	// Diagnostics with the same stage will sort together. See [Report.Sort].
	Stage int

	// When greater than zero, this will capture debugging information at the
	// site of each call to Error() etc. This will make diagnostic construction
	// orders of magnitude slower; it is intended to help tool writers to debug
	// their diagnostics.
	//
	// Higher values mean more debugging information. What debugging information
	// is actually provided is subject to change.
	Tracing int

	// If set, [Report.Sort] will not discard duplicate diagnostics, as defined
	// in that function's contract.
	KeepDuplicates bool

	// If set, all diagnostics of severity at most Warning (i.e., >= Warning
	// as integers) are suppressed.
	SuppressWarnings bool
}

Options for how a report should be constructed.

type Renderer

type Renderer struct {
	// If set, uses a compact one-line format for each diagnostic.
	Compact bool

	// If set, rendering results are enriched with ANSI color escapes.
	Colorize bool

	// Upgrades all warnings to errors.
	WarningsAreErrors bool

	// If set, remark diagnostics will be printed.
	ShowRemarks bool

	// If set, rendering a diagnostic will show the debug footer.
	ShowDebug bool
}

Renderer configures a diagnostic rendering operation.

func (Renderer) Render

func (r Renderer) Render(report *Report, out io.Writer) (errorCount, warningCount int, err error)

Render renders a diagnostic report.

In addition to returning the rendering result, returns whether the report contains any errors.

On the other hand, the actual error-typed return is an error when writing to the writer.

func (Renderer) RenderString

func (r Renderer) RenderString(report *Report) (text string, errorCount, warningCount int)

RenderString is a helper for calling Renderer.Render with a strings.Builder.

type Report

type Report struct {
	Options

	// The actual diagnostics on this report. Generally, you'll want to use one of
	// the helpers like [Report.Error] instead of appending directly.
	Diagnostics []Diagnostic
}

Report is a collection of diagnostics.

Report is not thread-safe (in the sense that distinct goroutines should not all write to Report at the same time). Instead, the recommendation is to create multiple reports and then merge them, using [Report.Sort] to canonicalize the result.

func (*Report) AnnotateICE

func (r *Report) AnnotateICE(options ...DiagnosticOption)

AnnotateICE will recover a panic and annotate it such that when [CatchICE] recovers it, it can extract this information and display it in the diagnostic.

func (*Report) AppendFromProto

func (r *Report) AppendFromProto(deserialize func(proto.Message) error) error

AppendFromProto appends diagnostics from a Protobuf message to this report.

deserialize will be called with an empty message that should be deserialized onto, which this function will then convert into [Diagnostic]s to populate the report with.

func (*Report) Canonicalize

func (r *Report) Canonicalize()

Canonicalize sorts this report's diagnostics according to an specific ordering criteria. Diagnostics are sorted by, in order:

1. File name of primary span. 2. SortOrder value. 3. Start offset of primary snippet. 4. End offset of primary snippet. 5. Diagnostic tag. 6. Textual content of error message.

Where diagnostics have no primary span, the file is treated as empty and the offsets are treated as zero.

These criteria ensure that diagnostics for the same file go together, diagnostics for the same sort order (lex, parse, etc) go together, and they are otherwise ordered by where they occur in the file.

Canonicalize will deduplicate diagnostics whose primary span and (nonempty) diagnostic tags are equal, selecting the diagnostic that sorts as greatest as the canonical value. This allows later diagnostics to replace earlier diagnostics, so long as they cooperate by using the same tag. Deduplication can be suppressed using Options.KeepDuplicates.

func (*Report) CatchICE

func (r *Report) CatchICE(resume bool, diagnose func(*Diagnostic))

CatchICE will recover a panic (an internal compiler error, or ICE) and log it as an error diagnostic. This function should be called in a defer statement.

When constructing the diagnostic, diagnose is called, to provide an opportunity to annotate further.

If resume is true, resumes the recovered panic.

func (*Report) Error

func (r *Report) Error(err Diagnose) *Diagnostic

Error pushes an error diagnostic onto this report.

func (*Report) Errorf

func (r *Report) Errorf(format string, args ...any) *Diagnostic

Errorf creates an ad-hoc error diagnostic with the given message; analogous to fmt.Errorf.

func (*Report) Fatalf

func (r *Report) Fatalf(format string, args ...any) *Diagnostic

Fatalf creates an ad-hoc ICE diagnostic with the given message; analogous to fmt.Errorf.

func (*Report) Level

func (r *Report) Level(level Level, err Diagnose) *Diagnostic

Level pushes a diagnostic with the given level onto this report.

func (*Report) Levelf

func (r *Report) Levelf(level Level, format string, args ...any) *Diagnostic

Levelf creates an ad-hoc diagnostic with the given level and message; analogous to fmt.Errorf.

func (*Report) Remark

func (r *Report) Remark(err Diagnose) *Diagnostic

Remark pushes a remark diagnostic onto this report.

func (*Report) Remarkf

func (r *Report) Remarkf(format string, args ...any) *Diagnostic

Remarkf creates an ad-hoc remark diagnostic with the given message; analogous to fmt.Errorf.

func (*Report) SaveOptions

func (r *Report) SaveOptions(body func())

SaveOptions calls the given function and, upon its completion, restores r.Options to the value it had before it was called.

func (*Report) SoftError

func (r *Report) SoftError(hard bool, err Diagnose) *Diagnostic

SoftError pushes a diagnostic with the onto this report, making it a warning if hard is false.

func (*Report) SoftErrorf

func (r *Report) SoftErrorf(hard bool, format string, args ...any) *Diagnostic

SoftError pushes an ad-hoc soft error diagnostic with the given message; analogous to fmt.Errorf.

func (*Report) ToProto

func (r *Report) ToProto() proto.Message

ToProto converts this report into a Protobuf message for serialization.

This operation is lossy: only the Diagnostics slice is serialized. It also discards concrete types of Diagnostic.Err, replacing them with opaque errors.New values on deserialization.

It will also deduplicate [File2] values based on their paths, paying no attention to their contents.

func (*Report) Warn

func (r *Report) Warn(err Diagnose) *Diagnostic

Warn pushes a warning diagnostic onto this report.

func (*Report) Warnf

func (r *Report) Warnf(format string, args ...any) *Diagnostic

Warnf creates an ad-hoc warning diagnostic with the given message; analogous to fmt.Errorf.

Directories

Path Synopsis
Package rtags defines publicly-exposed diagnostic tag constants for use with [report.Tag].
Package rtags defines publicly-exposed diagnostic tag constants for use with [report.Tag].

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL