diag

package
v0.0.0-...-8d9931c Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package diag holds the OpenAPI compiler's diagnostic vocabulary: the stable codes it reports under, and the single constructor that builds a diagnostic from them.

It sits at the bottom of the compiler's import graph because every package that reports anything reaches it and it reaches nothing but ir. The codes are format-specific strings and stay here rather than promoting to compilers/compile — that rule wants evidence from all three compilers, and the GraphQL and Protobuf drafts have not been compared. Whether Newf itself later promotes is left open for the same reason.

Index

Constants

View Source
const (
	// Validation reports a speakeasy validation finding; it is suffixed with the
	// library rule name (e.g. "openapi/validation/duplicate-tag").
	Validation = "openapi/validation"
	// UnsupportedVersion reports an OpenAPI version the compiler cannot lower.
	UnsupportedVersion = "openapi/unsupported-version"
	// UnresolvedRef reports a $ref that could not be resolved.
	UnresolvedRef = "openapi/unresolved-ref"
	// CyclicRef reports a degenerate reference cycle — a recursive YAML anchor, a
	// chain of $ref-only schemas that never reaches a concrete type, or a
	// reference whose pointer resolves through a reference already being resolved
	// — caught before it can crash the parser with a stack overflow or deadlock
	// the resolver on a lock its own goroutine holds.
	CyclicRef = "openapi/cyclic-ref"
	// CycleScanFailed reports that the pre-parse cycle scan did not run to
	// completion — either it aborted (a detector bug) or the document exceeded one
	// of its expansion bounds — leaving its stack-overflow protection incomplete
	// for the source. It is a warning, never a refusal: the compile still
	// proceeds, and every cycle the scan did classify is still caught.
	CycleScanFailed = "openapi/cycle-scan-failed"
	// SourceTooLarge reports a document with more YAML nodes than the pre-parse
	// scan indexes (sourceindex.MaxIndexedNodes). Every answer the index gives
	// about such a document is a partial one, including the node count the
	// alias-expansion allowance is derived from, so the document is refused rather
	// than scanned against a bound computed from a count that stopped early.
	SourceTooLarge = "openapi/source-too-large"
	// TaggedMapping reports a mapping carrying a YAML tag other than !!map — a
	// local `!content:`, or a standard tag naming another type — which OpenAPI
	// forbids: its YAML form limits tags to YAML 1.2's JSON schema ruleset, the
	// one that round-trips to JSON. The parser degrades one at a plain object
	// position to a type-mismatch finding, and at a path item or a callback,
	// whose models embed a map, drops the tag without a word; but at a reference
	// position whose model is a struct — a request body, a response, a
	// parameter, a header, an example, a link, a security scheme — it leaves the
	// model unbuilt and dereferences it, on a goroutine of its own that no
	// recover in this compiler reaches (GitHub #474). The document is refused
	// before the parser sees it, at every position rather than only the faulting
	// ones: telling them apart means maintaining a copy of the parser's object
	// model, and each place the copy drifted would be a crash again. A tagged
	// scalar is a different question (GitHub #245) and not refused here.
	TaggedMapping = "openapi/tagged-mapping"
	// StreamDocumentsDropped reports a YAML stream holding more than one
	// document with content. An OpenAPI document is one YAML document, so the
	// first is what is lowered; every one after it reaches the IR in no form at
	// all, which is a losslessness failure rather than a degradation (the
	// distinction UnpreservableConstruct draws) and so an error, though the
	// compile proceeds with the first (GitHub #387). A document that holds
	// nothing — a bare separator, an explicit null — drops nothing and is not
	// reported.
	StreamDocumentsDropped = "openapi/stream-documents-dropped"
	// UndecodableSource reports a source that declares one of this compiler's
	// discriminating keys and does not parse as YAML or JSON. It is reported from
	// detection rather than from the compile, because a document that cannot be
	// parsed never reaches one: without it the engine could only say no compiler
	// recognized the source, which is wrong twice over — this compiler did
	// recognize it, and the reason it declined is the parse error it holds.
	UndecodableSource = "openapi/undecodable-source"
	// OverlayInvalid reports an overlay document that could not be parsed, or that
	// parsed but is not a valid Overlay — a missing version, no actions, an action
	// naming no target. Nothing is applied, so the compile refuses rather than
	// lowering a source the caller believes was patched.
	OverlayInvalid = "openapi/invalid-overlay"
	// OverlayFailed reports an overlay the library could not apply as written: a
	// selector matching nothing under strict application, or an action whose
	// update disagrees with the shape it targets. Actions are applied in order and
	// the ones that landed are not undone, so the compile refuses — the tree left
	// behind is neither the source nor what the overlay asked for.
	OverlayFailed = "openapi/overlay-failed"
	// OverlayAction reports one strict-mode finding about a single action, naming
	// the action and its target: that it changed nothing, or that it relies on
	// JSONPath behaviour the overlay did not opt into.
	//
	// It stands alone as often as it accompanies a refusal, and the difference is
	// the point. An action whose selector matched nothing is a typo and refuses
	// under OverlayFailed, with these naming which actions are why; an action that
	// matched and then changed nothing is merely redundant — the fix it describes
	// is already in the source — so it is reported and the compile proceeds.
	OverlayAction = "openapi/overlay-action"
	// OverlayOriginIncomplete reports that the overlay applied but the walk that
	// attributes positions to it exceeded its node budget. Provenance degrades to
	// naming the source for every position, which is what a compile with no
	// overlay reports; nothing else about the lowering changes.
	OverlayOriginIncomplete = "openapi/overlay-origin-incomplete"
	// ValidationOnlyKeyword reports a validation-only JSON Schema keyword kept
	// verbatim under Unmodeled (ir-design §4.7).
	ValidationOnlyKeyword = "openapi/validation-only-keyword"
	// FalseSchema reports a boolean `false` schema (matches nothing).
	FalseSchema = "openapi/false-schema"
	// EmptyEnum reports an `enum` whose member list is empty. JSON Schema allows
	// it and gives it a meaning — the value space holds no member, so the
	// position accepts no instance at all — which the IR states exactly, as a
	// closed Enum with no members.
	//
	// Warning rather than info, and the split from FalseSchema beside it is the
	// reason. A boolean `false` schema is the idiom for "forbid this here", so
	// announcing the lowering is all a reader needs; an empty member list is the
	// same statement written the way nobody writes it on purpose, and it is what
	// a generator emitting a list it never filled produces. Every position
	// reaching it is uncallable, so the document is told rather than merely
	// recorded. Not an error: the document is well-formed, and harness.Check
	// stops at the first error diagnostic, which would hide every later finding
	// in the same spec.
	EmptyEnum = "openapi/empty-enum"
	// NumericPrecision reports a numeric bound literal that is not a finite
	// number (error severity: Morphic owns these keywords, so this is the sole
	// diagnostic for the defect — see boundLiteralDiag).
	NumericPrecision = "openapi/invalid-numeric-literal"
	// ExclusiveBoundForm reports an exclusiveMinimum/exclusiveMaximum whose value
	// form is wrong for the document's dialect (a boolean under 2020-12, or a
	// number under 3.0) — see exclusiveFormDiag.
	ExclusiveBoundForm = "openapi/invalid-exclusive-bound"
	// InvalidStatusKey reports a responses-map key that names no status: not a
	// 100–599 code, not one of the 1XX–5XX wildcard ranges, not "default". The
	// response still lowers, with no status condition rather than the catch-all
	// range that "default" alone denotes (GitHub #262).
	InvalidStatusKey = "openapi/invalid-status-key"
	// DuplicateStatusKey reports two responses-map keys on one operation that
	// resolve to the same status range — "4XX" beside "4xx", or "200" beside a
	// second "200" a merge key introduced. The key reaches the IR neutralized, so
	// both lower to one hint and one condition, and an ErrorCase carries no ID:
	// name and conditions are the whole of what tells two apart. Both are kept,
	// because neither key is wrong on its own and dropping one would pick a winner
	// on nothing but declaration order.
	DuplicateStatusKey = "openapi/duplicate-status-key"
	// InvalidMethodKey reports an additionalOperations key that names no method:
	// the empty string. The operation still lowers, binding the key as written, so
	// nothing the entry declares is lost — what is reported is that the binding's
	// method is unusable. speakeasy rejects a key naming a *standard* method, which
	// belongs in its own field, but accepts this one.
	InvalidMethodKey = "openapi/invalid-method-key"
	// DegradedConstruct reports a construct the compiler could not carry into the
	// IR as written: preserved raw for want of a structural home, lowered to a
	// weaker shape (a heterogeneous enum as a union, an unconvertible value as
	// the top type), or — for an annotation like a default or example — dropped.
	// It marks the lossy lowerings the compiler reports, not a guarantee that
	// every lossy lowering is reported.
	DegradedConstruct = "openapi/degraded-construct"
	// CompositionLowering reports that a schema conjoining a structural body with
	// a oneOf/anyOf was lowered by distributing the body across the union
	// variants (ir-design §4.3, §4.8). Nothing is lost, so this records a
	// decision rather than a degradation: the IR's shape no longer mirrors the
	// source's, which is what a reader comparing the two needs told.
	CompositionLowering = "openapi/composition-lowering"
	// DynamicRefExpanded reports that a $dynamicRef was resolved to the one
	// $dynamicAnchor matching it in this document (ir-design §4.7). Like
	// CompositionLowering this records a decision rather than a loss: the
	// reference resolved, but what the source wrote as dynamic is now a fixed
	// target, so a reader comparing IR to source needs telling that the
	// indirection was collapsed at compile time rather than left to evaluation.
	DynamicRefExpanded = "openapi/dynamic-ref-expanded"
	// ConflictingRedecl reports that inline allOf branches redeclare one field
	// with values that disagree: an incompatible target type (string vs. integer)
	// is unsatisfiable outright, while a conflicting constraint keyword (e.g.
	// minimum: 10 vs. exclusiveMinimum: 10) is usually still satisfiable but not
	// representable by a simple merge, so the merge keeps an arbitrary
	// source-order winner — possibly the looser bound — and surfaces the
	// disagreement instead of silently discarding it.
	//
	// Either way the losing declaration is kept whole under the merged
	// property's Unmodeled, so it survives for a consumer reading the document
	// rather than the diagnostic stream (merge.keepLosingDeclaration). A
	// constraint conflict is kept there until the bounds are intersected
	// instead (GitHub #10), which is the recorded direction; the entry records
	// what the merge dropped, not what it should have kept.
	ConflictingRedecl = "openapi/conflicting-redeclaration"
	// DisjointVisibility reports one field restricted to lifecycle sets that
	// share nothing — readOnly against writeOnly — so no lifecycle admits it at
	// all. Both spellings raise it under this one code: one schema writing both
	// flags, and inline allOf branches whose intersection is empty. It is
	// neither of its neighbours: ConflictingRedecl keeps an arbitrary
	// source-order winner, and DegradedConstruct lowers to a weaker shape,
	// whereas Visibility{None: true} is the exact answer and a shape the
	// IR already has. It is reported nonetheless, because a field no request or
	// response can carry is seldom what the document set out to say.
	DisjointVisibility = "openapi/disjoint-visibility"
	// AliasAmplification reports a document whose YAML aliases expand it past a
	// fixed multiple of its own size (scan's maxAliasAmplification, with a floor
	// for small documents) — a billion-laughs shape
	// that would exhaust memory inside soa.Unmarshal before ResolveAllReferences
	// ever runs (GitHub #27). Unlike CycleScanFailed's incomplete-scan warning,
	// this is a positive, measured finding, so the document is refused outright
	// rather than handed to the parser.
	//
	// It is a ratio and no caller's budget, so no setting admits a document it
	// names. A document inside the ratio whose aliases still add more nodes than
	// openapi.Limits.MaxAliasSurplus is BudgetExceeded instead, and one past both
	// is this.
	AliasAmplification = "openapi/alias-amplification"
	// BudgetExceeded reports an input that crossed one of the compiler's
	// cardinality budgets: a source or overlay document past the byte budget, a
	// source document past the node budget or an overlay action that would build
	// it past that, a source or overlay document whose aliases add more nodes
	// than the alias budget, or a single enum past the member budget (GitHub #75).
	//
	// It is the size axis of the same family AliasAmplification belongs to, and
	// deliberately a separate code, because what it refuses is different in kind.
	// AliasAmplification names a document that is small until it is expanded — a
	// bomb, and never a shape an author writes on purpose. These name a document
	// that is honestly, legally that large, so the finding is "past the budget
	// this compile was given", not "malicious": every one of them is raised
	// through openapi.Limits by a caller who has the memory for it.
	//
	// Error rather than a degradation at every site. The load-phase budgets
	// refuse the document outright — nothing is lowered, so there is no weaker
	// shape to report. The enum budget does leave a node behind, the top type,
	// but every member the source declared is gone from it, which is a
	// losslessness failure rather than a lossy lowering (the distinction
	// UnpreservableConstruct draws).
	BudgetExceeded = "openapi/budget-exceeded"
	// UnattachableRequired reports a composition-scope `required` name (an allOf
	// branch's own required list, or the composed schema's own) that matches none
	// of the model's own properties, so it has no IR home to attach to
	// (ir-design §4.3: Properties holds only own properties, and flattening
	// across Base/Mixins is computed, never stored).
	UnattachableRequired = "openapi/unattachable-required"
	// InternalInvariant reports that lowering's own pointer-to-registry invariant
	// broke: a pointer named a type ID the registry does not hold, so whatever
	// was about to be attached there had nowhere to go. No source can provoke
	// this — it is a compiler bug — but it is reported rather than dropped in
	// silence, since the alternative is losing constructs with no trace at all.
	InternalInvariant = "openapi/internal-invariant"
	// DuplicateOperationID reports an operationId that one declaration claims
	// more than once because it is mounted more than once: a path item or an
	// operation reused by a $ref, a YAML alias or a merge key. The document
	// writes the id once, so it is a warning, but each mount is an operation of
	// its own and an emitter renders them all under one identifier.
	DuplicateOperationID = "openapi/duplicate-operation-id"
	// ConflictingOperationID reports an operationId that a second declaration
	// writes again. OpenAPI requires the id to be unique among every operation
	// the document describes, webhooks and callbacks included, and this is the
	// document repeating it, so it is an error (GitHub #502).
	ConflictingOperationID = "openapi/conflicting-operation-id"
	// IncompleteSecurityScheme reports a securitySchemes entry that omits the
	// field naming which authentication mechanism it is — `type`, or the RFC 7235
	// `scheme` token that is the mechanism when the type is http. The entry
	// declares a scheme without saying what it does, so nothing is interned for
	// it and every requirement naming it is dropped (GitHub #294).
	//
	// Error rather than a degradation, for the reason UnresolvedRef is one at the
	// neighbouring shape: the entry reached the IR in no form at all, so a reader
	// told only that it was degraded would go looking for a scheme that is not
	// there. The document is invalid either way — OpenAPI requires both fields —
	// so this hides no later finding the loader's own refusal would not have.
	IncompleteSecurityScheme = "openapi/incomplete-security-scheme"
	// ReservedHeaderName reports a header declaration OpenAPI says SHALL be
	// ignored, because the name restates something the protocol layer already
	// owns. The specification states the rule at three positions, and this code
	// covers all three rather than the one it was first noticed at:
	//
	//   - §4.8.12, a parameter with `in: header` named Accept, Content-Type or
	//     Authorization — content negotiation, the request body's media type, the
	//     security scheme's credential;
	//   - §4.8.17, a Content-Type entry in a response's `headers` map, whose
	//     media type the response's own `content` map already names;
	//   - §4.8.15, a Content-Type entry in an encoding's `headers` map, which the
	//     encoding's own `contentType` describes separately.
	//
	// The compiler keeps the declaration in every case, because dropping declared
	// content is a loss and choosing between the two is an emitter's call, not a
	// compiler's (invariant 2). The diagnostic is what makes the deviation from
	// the SHALL visible, so an emitter can suppress the declaration rather than
	// generate one that fights what it collides with (GitHub #39).
	//
	// Unconditional, and deliberately not behind an Options switch: invariant 6
	// governs what is *inferred*, and nothing here is. The names are fixed by the
	// specification, the comparison is against a declared name, and the document
	// lowers byte-for-byte the same whether or not this fires — so there is no
	// inference to mark Inferred and no semantics to disable. Warning rather than
	// info because the document really did write something the spec says has no
	// effect; error is wrong twice over, since the document is well-formed and
	// harness.Check stops at the first error diagnostic, which would hide every
	// later finding in the same spec.
	ReservedHeaderName = "openapi/reserved-header-name"
	// UnpreservableConstruct reports a construct that reached the IR in no form at
	// all: the compiler had no field to model it and its source node could not be
	// converted to JSON either, so Unmodeled could not hold it. It is an error
	// because it is a losslessness failure rather than a degradation —
	// DegradedConstruct's constructs survive in a weaker shape, and these survive
	// in none (GitHub #144).
	UnpreservableConstruct = "openapi/unpreservable-construct"
	// UnknownSchemaKeyword reports a JSON Schema keyword no field of the schema
	// model names, kept verbatim under Unmodeled.
	//
	// Info, because the document did nothing wrong: JSON Schema requires an
	// implementation to ignore a keyword it does not recognize, and says such a
	// keyword may carry meaning for other tooling, so an unrecognized keyword is
	// legal input rather than a defect. What is recorded is this compiler's own
	// decision — that it read no meaning from the keyword and kept the text — which
	// is the same thing ValidationOnlyKeyword records beside it.
	UnknownSchemaKeyword = "openapi/unknown-schema-keyword"
	// UnknownObjectKey reports a key on an OpenAPI object that the specification
	// neither defines nor admits as an extension, kept verbatim under Unmodeled.
	//
	// Warning rather than info, because unlike its schema neighbour this one is a
	// defect: OpenAPI gives its objects a closed key set and requires every
	// extension to be prefixed x-, so a key that is neither is a document error —
	// in practice a misspelling of the field beside it, which is precisely the
	// class of mistake that survives when the compiler swallows the key in silence.
	//
	// Warning rather than error for the reason ReservedHeaderName is one: the
	// document still lowers, everything the key was written beside is unaffected,
	// and harness.Check stops at the first error diagnostic, which would hide every
	// later finding in the same spec and make any fixture carrying a stray key
	// unable to reach the invariant checks.
	UnknownObjectKey = "openapi/unknown-object-key"
	// UnknownKeyBudget reports an object declaring more keys the model does not
	// name than the compiler keeps, so the ones past the bound reached the IR in no
	// form at all.
	//
	// Every collection here is bounded, and this one is over a key set the document
	// chooses the size of. The bound is far above what any document writes by
	// accident, so tripping it is either a generated file or a hostile one; the
	// diagnostic is what keeps the discarded remainder from being a silent loss.
	UnknownKeyBudget = "openapi/unknown-key-budget"
	// UnknownKeyUnreachable reports a key the parsed model reported as undeclared
	// whose value the raw mapping does not present, so nothing of it reached the
	// IR.
	//
	// Distinct from UnpreservableConstruct beside it, which is a value that was
	// found and could not be rendered. This one was never reached: the parser
	// reads a mapping through its `<<` merge keys and the raw readers here do not,
	// so a merged-in key is named by the census and has no pair to read. Warning
	// rather than that one's error because such a document is legal and still
	// lowers.
	UnknownKeyUnreachable = "openapi/unknown-key-unreachable"
	// UnknownKeyEntryTaken reports a key whose Unmodeled entry is already held by
	// a construct declared somewhere else, so the key reached the IR in no form.
	//
	// The carriers holding more than one object's entries are where two constructs
	// can spell one entry: a parameter's own keys and the keywords its schema had
	// no home for share one unscoped namespace on ir.Parameter. Warning rather
	// than error because the document is otherwise lowered whole, and the entry
	// that did survive is in it.
	UnknownKeyEntryTaken = "openapi/unknown-key-entry-taken"
	// InvalidLocationKeyword reports a serialization keyword OpenAPI 3.2 forbids
	// at the parameter location that declares it: explode or allowReserved at
	// in: querystring, where the location binds the whole query string from the
	// parameter's content and states its serialization through the media type
	// alone, leaving nothing for either keyword to qualify.
	//
	// The bundled parser enforces this rule for style at that location but not
	// for its two neighbours (GitHub #408), so this compiler reports the gap
	// itself rather than relying on a validation finding that never arrives. The
	// value still lowers as declared: dropping content the document states is an
	// emitter's call, not a compiler's (invariant 2), the same choice already
	// made for style at this position.
	//
	// Warning, not the error style gets: style's finding is the parser's own
	// refusal-class validation, raised before this compiler ever sees the
	// document. This one is the opposite shape — the compiler already kept a
	// value it lowered and is saying so — the same class as
	// ReservedHeaderName and InvalidMethodKey beside it. All three keywords
	// are reported; a caller who wants the document refused over it has
	// --fail-on warning for that.
	InvalidLocationKeyword = "openapi/invalid-location-keyword"
)

The stable codes the OpenAPI compiler reports under. They are stable strings so CI can allowlist them (ir-design §13).

View Source
const MaxQuotedErrorBytes = 4 << 10

MaxQuotedErrorBytes bounds what a foreign error contributes to a diagnostic message. A diagnostic is read by a person and stored by a log, and an error raised by a library obeys neither: yaml.v3 reports a duplicated mapping key once per prior occurrence of it, so a 32 KB source repeating one key 6,553 times raises an error of 1.2 GB. Quoting that whole is not a report.

The cap is generous because the errors worth quoting are lists — the overlay validator writes one sentence per finding — and a list cut to its first entry says less than the reader came for. What a cut costs is the tail; what it buys is that a message is always a message.

Variables

This section is empty.

Functions

func HasError

func HasError(diags []ir.Diagnostic) bool

HasError reports whether any diagnostic carries error severity. The load phase uses it to tell a refusal (a real spec problem, e.g. a degenerate cycle) from advisory warnings it must carry forward rather than abort on.

func Newf

func Newf(sev ir.Severity, code string, prov ir.Provenance, format string, args ...any) ir.Diagnostic

Newf builds an ir.Diagnostic with a formatted message. It is the single constructor for this compiler's diagnostics, so severity, code and provenance are always populated.

func OneLine

func OneLine(err error) string

OneLine collapses err's text onto a single line, for a diagnostic that carries an error raised by something else, and cuts it at MaxQuotedErrorBytes.

A diagnostic is rendered one per line, so an embedded newline splits one report into several — and every line after the first carries no severity, code or location, which reads as a malformed diagnostic to anything parsing stderr. Both libraries this compiler reports through write multi-line errors: yaml.v3 as a header plus one indented line per finding, the overlay validator as a flat list of sentences.

Parts are joined with "; " so a flat list reads as a list, except after a part that already ends in a colon, where the next line is that header's content and a semicolon would read as a break in it.

The scan stops at the cap rather than trimming afterwards, so the work is bounded by what is kept and not by what the library wrote.

Types

This section is empty.

Jump to

Keyboard shortcuts

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