Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ linters:
- gocritic
- misspell
- nilerr
- nolintlint # a suppression outliving what it suppressed is invisible without this
- prealloc
- revive
- unconvert
Expand All @@ -22,6 +23,19 @@ linters:
lines: 70
statements: -1
ignore-comments: true
nolintlint:
# A directive that suppresses nothing reads as a live constraint on the code
# under it, and nothing else in the gate can tell the two apart: the tree
# stays green whether the finding is real or long gone. Each of these makes
# one way of writing an inert directive fail instead.
#
# allow-unused only reaches directives naming an ENABLED linter. One naming a
# disabled or nonexistent linter is ignored by the nolint processor and
# reported by nothing — which is how //nolint:forcetypeassert stood here
# without ever suppressing anything. That gap is GitHub #306.
allow-unused: false # it no longer suppresses anything — delete it
require-specific: true # bare //nolint hides findings nobody chose to accept
require-explanation: true # the rationale is what a later reader re-checks against
gocognit:
# Calibrated against the finished tree, whose worst function scores 21.
# Raising this is how a function that should have been split stays whole,
Expand Down
54 changes: 33 additions & 21 deletions compilers/openapi/internal/annotation/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import (
"github.com/dexpace/morphic/ir"
)

// boundSide names which of a numeric constraint's two sides a read applies to.
// It is a named type rather than a bool because applyExclusive takes a dialect
// flag beside it, and two bare bools in a row say nothing at the call site about
// which is which.
type boundSide int

const (
minBound boundSide = iota // minimum / exclusiveMinimum
maxBound // maximum / exclusiveMaximum
)

// Constraints reads a schema's scalar (string/number/object-count) value
// constraints into an ir.Constraints. Numeric bounds are read from the raw YAML
// nodes, never the *float64 model fields, to preserve full decimal precision
Expand All @@ -30,8 +41,8 @@ func Constraints(s *oas3.Schema, exclusiveBoolean bool) (*ir.Constraints, []ir.D
}
c := &ir.Constraints{}
diags := numericBounds(c, s)
diags = append(diags, applyExclusive(c, s, true, exclusiveBoolean)...)
diags = append(diags, applyExclusive(c, s, false, exclusiveBoolean)...)
diags = append(diags, applyExclusive(c, s, minBound, exclusiveBoolean)...)
diags = append(diags, applyExclusive(c, s, maxBound, exclusiveBoolean)...)
c.MinLength = s.MinLength
c.MaxLength = s.MaxLength
c.Pattern = s.GetPattern()
Expand Down Expand Up @@ -84,13 +95,14 @@ func boundLiteralDiag(prop, literal string, err error) ir.Diagnostic {
// 3.0 boolean arm flags the corresponding Min/Max as exclusive; the 2020-12
// numeric arm (3.1/3.2) carries the bound value itself, read from the raw node to
// avoid the float64 trap, and hands it to reconcileBound, which decides how it
// meets any minimum/maximum declared beside it. exclusiveBoolean selects the
// dialect (true for 3.0). Because load suppresses the library's type-mismatch
// on these keywords, a value in the wrong form for the dialect is reported and
// dropped here rather than silently accepted.
func applyExclusive(c *ir.Constraints, s *oas3.Schema, isMin, exclusiveBoolean bool) []ir.Diagnostic {
// meets any minimum/maximum declared beside it. side picks which of the two
// keywords is read; exclusiveBoolean selects the dialect (true for 3.0).
// Because load suppresses the library's type-mismatch on these keywords, a
// value in the wrong form for the dialect is reported and dropped here rather
// than silently accepted.
func applyExclusive(c *ir.Constraints, s *oas3.Schema, side boundSide, exclusiveBoolean bool) []ir.Diagnostic {
ev, prop := s.GetExclusiveMaximum(), "exclusiveMaximum"
if isMin {
if side == minBound {
ev, prop = s.GetExclusiveMinimum(), "exclusiveMinimum"
}
if ev == nil {
Expand All @@ -101,7 +113,7 @@ func applyExclusive(c *ir.Constraints, s *oas3.Schema, isMin, exclusiveBoolean b
}
if ev.IsLeft() {
if b := ev.GetLeft(); b != nil && *b {
setExclusiveFlag(c, isMin)
setExclusiveFlag(c, side)
}
return nil
}
Expand All @@ -113,7 +125,7 @@ func applyExclusive(c *ir.Constraints, s *oas3.Schema, isMin, exclusiveBoolean b
if err != nil {
return []ir.Diagnostic{boundLiteralDiag(prop, node.Value, err)}
}
return reconcileBound(c, isMin, v)
return reconcileBound(c, side, v)
}

// reconcileBound settles one side's bound when the 2020-12 dialect declares
Expand All @@ -133,23 +145,23 @@ func applyExclusive(c *ir.Constraints, s *oas3.Schema, isMin, exclusiveBoolean b
// callers route what it returns to different carriers — a property, a parameter
// and a hoisted alias node — so opening one is a change of its own, tracked in
// GitHub #286 rather than made here.
func reconcileBound(c *ir.Constraints, isMin bool, excl ir.BigVal) []ir.Diagnostic {
func reconcileBound(c *ir.Constraints, side boundSide, excl ir.BigVal) []ir.Diagnostic {
incl, inclProp, exclProp := c.Max, "maximum", "exclusiveMaximum"
if isMin {
if side == minBound {
incl, inclProp, exclProp = c.Min, "minimum", "exclusiveMinimum"
}
if incl == nil {
setExclusiveBound(c, isMin, &excl)
setExclusiveBound(c, side, &excl)
return nil
}

tighter, compared := inclusiveIsTighter(*incl, excl, isMin)
tighter, compared := inclusiveIsTighter(*incl, excl, side)
if tighter {
return []ir.Diagnostic{redundantBoundDiag(inclProp, *incl, exclProp, excl, compared)}
}

dropped := *incl
setExclusiveBound(c, isMin, &excl)
setExclusiveBound(c, side, &excl)
return []ir.Diagnostic{redundantBoundDiag(exclProp, excl, inclProp, dropped, compared)}
}

Expand All @@ -176,7 +188,7 @@ func reconcileBound(c *ir.Constraints, isMin bool, excl ir.BigVal) []ir.Diagnost
// grammar is the narrower of the two — so it stands for the day that changes:
// a bound this cannot order is one that could be silently replaced by the looser
// of its pair, which is the defect this reconciliation exists to prevent.
func inclusiveIsTighter(incl, excl ir.BigVal, isMin bool) (tighter, compared bool) {
func inclusiveIsTighter(incl, excl ir.BigVal, side boundSide) (tighter, compared bool) {
inclDec, inclOK := parseDecimalBound(incl)
exclDec, exclOK := parseDecimalBound(excl)
if !inclOK || !exclOK {
Expand All @@ -186,7 +198,7 @@ func inclusiveIsTighter(incl, excl ir.BigVal, isMin bool) (tighter, compared boo
if order == 0 {
return false, true
}
return (order > 0) == isMin, true
return (order > 0) == (side == minBound), true
}

// redundantBoundDiag reports the co-declared 2020-12 bound that did not reach
Expand Down Expand Up @@ -226,8 +238,8 @@ func exclusiveFormDiag(prop string, exclusiveBoolean bool) ir.Diagnostic {
}

// setExclusiveFlag marks the low or high bound exclusive.
func setExclusiveFlag(c *ir.Constraints, isMin bool) {
if isMin {
func setExclusiveFlag(c *ir.Constraints, side boundSide) {
if side == minBound {
c.ExclusiveMin = true
return
}
Expand All @@ -238,8 +250,8 @@ func setExclusiveFlag(c *ir.Constraints, isMin bool) {
// replacing whatever minimum/maximum put there. Only reconcileBound may call it,
// which is where the replacement is decided; calling it directly is the shape of
// GitHub #33.
func setExclusiveBound(c *ir.Constraints, isMin bool, v *ir.BigVal) {
if isMin {
func setExclusiveBound(c *ir.Constraints, side boundSide, v *ir.BigVal) {
if side == minBound {
c.Min = v
c.ExclusiveMin = true
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestApplyExclusive_NumericWithoutRootNode(t *testing.T) {
f := 5.0
s := &oas3.Schema{ExclusiveMinimum: &values.EitherValue[bool, bool, float64, float64]{Right: &f}}
c := &ir.Constraints{}
diags := applyExclusive(c, s, true, false)
diags := applyExclusive(c, s, minBound, false)
// The numeric arm is taken (2020-12 dialect, numeric value) but there is no raw
// node to read the exact literal from, so nothing is set and no diagnostic.
assert.Nil(t, diags)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ func TestReconcileBound_ABoundNoDecimalReadingOrdersKeepsTheExclusiveOne(t *test
t.Parallel()
c := &ir.Constraints{Min: bigOf("1p4")}

diags := reconcileBound(c, true, ir.BigVal("5"))
diags := reconcileBound(c, minBound, ir.BigVal("5"))

want := ir.Constraints{Min: bigOf("5"), ExclusiveMin: true}
if diff := cmp.Diff(want, *c); diff != "" {
Expand Down
8 changes: 3 additions & 5 deletions compilers/openapi/internal/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ type Document struct {
// become ir.Diagnostic values; the Go error return is reserved for I/O and
// programmer errors (a hard unmarshal failure). A nil document with diagnostics
// signals a refusal to lower (unsupported version) without aborting the batch.
//
//nolint:unparam // srcIndex varies once Compile drives the multi-source loop
func Load(ctx context.Context, srcIndex int, src compilers.Source, opts Options) (*Document, []ir.Diagnostic, error) {
// An overlay sharing the source's index is the one way to get the attribution
// silently wrong. Every position the overlay introduced would name the source,
Expand Down Expand Up @@ -479,9 +477,9 @@ func joinedParts(err error) []error {
if err == nil {
return nil
}
//nolint:errorlint // Matched at the top level by construction — the join is
// what ResolveAllReferences returns. errors.As would walk further into
// speakeasy error types whose As method panics; see asValidationError.
// Matched at the top level by construction — the join is what
// ResolveAllReferences returns. errors.As would walk further into speakeasy
// error types whose As method panics; see asValidationError.
if multi, ok := err.(interface{ Unwrap() []error }); ok {
return multi.Unwrap()
}
Expand Down
2 changes: 1 addition & 1 deletion pass/validate_propids_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestValidate_PropIDMapKeyIsLeftToTheEncodingCheck(t *testing.T) {
// tagging prop.
func discriminatedDoc(prop ir.PropID) *ir.Document {
doc := validDoc()
base := doc.Types["t/m"].(*ir.Model) //nolint:forcetypeassert // validDoc builds it
base := doc.Types["t/m"].(*ir.Model) // validDoc builds it
base.Discriminator = &ir.Discriminator{Property: prop}
return doc
}
Expand Down
Loading