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:
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.
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.
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.
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.
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".
Error messages should be succinct: short and sweet, keeping in mind (1). Users will see these messages many, many times.
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.
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.
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.
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).
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 ¶
- Variables
- type Diagnose
- type Diagnostic
- func (d *Diagnostic) Apply(options ...DiagnosticOption) *Diagnostic
- func (d *Diagnostic) Debug() []string
- func (d *Diagnostic) File() string
- func (d *Diagnostic) Help() []string
- func (d *Diagnostic) Is(tag string) bool
- func (d *Diagnostic) Level() Level
- func (d *Diagnostic) Message() string
- func (d *Diagnostic) Notes() []string
- func (d *Diagnostic) Primary() source.Span
- func (d *Diagnostic) Tag() string
- type DiagnosticOption
- func Debugf(format string, args ...any) DiagnosticOption
- func Helpf(format string, args ...any) DiagnosticOption
- func InFile(path string) DiagnosticOption
- func Message(format string, args ...any) DiagnosticOption
- func Notef(format string, args ...any) DiagnosticOption
- func Snippet(at source.Spanner) DiagnosticOption
- func Snippetf(at source.Spanner, format string, args ...any) DiagnosticOption
- func SuggestEdits(at source.Spanner, message string, edits ...Edit) DiagnosticOption
- func SuggestEditsWithWidening(at source.Spanner, message string, edits ...Edit) DiagnosticOption
- func Tag(t string) DiagnosticOption
- type Edit
- type Level
- type Options
- type Renderer
- type Report
- func (r *Report) AnnotateICE(options ...DiagnosticOption)
- func (r *Report) AppendFromProto(deserialize func(proto.Message) error) error
- func (r *Report) Canonicalize()
- func (r *Report) CatchICE(resume bool, diagnose func(*Diagnostic))
- func (r *Report) Error(err Diagnose) *Diagnostic
- func (r *Report) Errorf(format string, args ...any) *Diagnostic
- func (r *Report) Fatalf(format string, args ...any) *Diagnostic
- func (r *Report) Level(level Level, err Diagnose) *Diagnostic
- func (r *Report) Levelf(level Level, format string, args ...any) *Diagnostic
- func (r *Report) Remark(err Diagnose) *Diagnostic
- func (r *Report) Remarkf(format string, args ...any) *Diagnostic
- func (r *Report) SaveOptions(body func())
- func (r *Report) SoftError(hard bool, err Diagnose) *Diagnostic
- func (r *Report) SoftErrorf(hard bool, format string, args ...any) *Diagnostic
- func (r *Report) ToProto() proto.Message
- func (r *Report) Warn(err Diagnose) *Diagnostic
- func (r *Report) Warnf(format string, args ...any) *Diagnostic
Constants ¶
This section is empty.
Variables ¶
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 ¶
IsDeletion returns whether this edit involves deleting part of the source text.
func (Edit) IsInsertion ¶
IsInsertion returns whether this edit involves inserting new text.
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 ¶
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 ¶
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 ¶
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 ¶
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.