Skip to content

Commit

Permalink
feat: improve arktype discrimination, attest instantiations and compl…
Browse files Browse the repository at this point in the history
…etions (#1011)
  • Loading branch information
ssalbdivad authored Jun 13, 2024
1 parent ee486ef commit 2be4f5b
Show file tree
Hide file tree
Showing 42 changed files with 734 additions and 492 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-ties-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arktype/util": patch
---

### Add a new `arrayEquals` util for shallow array comparisons
5 changes: 5 additions & 0 deletions .changeset/holy-moly-guacamole.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arktype/schema": patch
---

### Improve discrimination logic across several issues (see primary release notes at [ArkType's CHANGELOG](../type/CHANGELOG.md))
71 changes: 71 additions & 0 deletions .changeset/thin-boxes-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
"@arktype/attest": minor
---

### Throw by default when attest.instantiations() exceeds the specified benchPercentThreshold

Tests like this will now correctly throw inline instead of return a non-zero exit code:

```ts
it("can snap instantiations", () => {
type Z = makeComplexType<"asbsdfsaodisfhsda">
// will throw here as the actual number of instantiations is more
// than 20% higher than the snapshotted value
attest.instantiations([1, "instantiations"])
})
```

### Snapshotted completions will now be alphabetized

This will help improve stability, especially for large completion lists like this one which we updated more times than we'd care to admit 😅

```ts
attest(() => type([""])).completions({
"": [
"...",
"===",
"Array",
"Date",
"Error",
"Function",
"Map",
"Promise",
"Record",
"RegExp",
"Set",
"WeakMap",
"WeakSet",
"alpha",
"alphanumeric",
"any",
"bigint",
"boolean",
"creditCard",
"digits",
"email",
"false",
"format",
"instanceof",
"integer",
"ip",
"keyof",
"lowercase",
"never",
"null",
"number",
"object",
"parse",
"semver",
"string",
"symbol",
"this",
"true",
"undefined",
"unknown",
"uppercase",
"url",
"uuid",
"void"
]
})
```
2 changes: 1 addition & 1 deletion ark/attest/__tests__/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contextualize(() => {
it(".type.completions", () => {
//@ts-expect-error
attest({ ark: "s" } as Arks).type.completions({
s: ["string", "symbol", "semver"]
s: ["semver", "string", "symbol"]
})
})

Expand Down
2 changes: 1 addition & 1 deletion ark/attest/__tests__/demo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ contextualize(() => {
// snapshot expected completions for any string literal!
// @ts-expect-error (if your expression would throw, prepend () =>)
attest(() => type({ a: "a", b: "b" })).completions({
a: ["any", "alpha", "alphanumeric"],
a: ["alpha", "alphanumeric", "any"],
b: ["bigint", "boolean"]
})
type Legends = { faker?: "🐐"; [others: string]: unknown }
Expand Down
10 changes: 9 additions & 1 deletion ark/attest/__tests__/instantiations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type } from "arktype"
import { it } from "mocha"

contextualize(() => {
it("Inline instantiations", () => {
it("inline", () => {
const user = type({
kind: "'admin'",
"powers?": "string[]"
Expand All @@ -17,4 +17,12 @@ contextualize(() => {
})
attest.instantiations([7574, "instantiations"])
})
it("fails on instantiations above threshold", () => {
attest(() => {
const user = type({
foo: "0|1|2|3|4|5|6"
})
attest.instantiations([1, "instantiations"])
}).throws("exceeded baseline by")
})
})
8 changes: 8 additions & 0 deletions ark/attest/__tests__/snapExpectedOutput.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { attest, cleanup, setup } from "@arktype/attest"
import type { makeComplexType } from "./utils.js"

setup()

Expand All @@ -23,4 +24,11 @@ multiline`)

attest("with `quotes`").snap("with `quotes`")

const it = (name: string, fn: () => void) => fn()

it("can snap instantiations", () => {
type Z = makeComplexType<"asbsdfsaodisfhsda">
attest.instantiations([229, "instantiations"])
})

cleanup()
8 changes: 8 additions & 0 deletions ark/attest/__tests__/snapTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { attest, cleanup, setup } from "@arktype/attest"
import type { makeComplexType } from "./utils.js"

setup()

Expand All @@ -22,4 +23,11 @@ attest("multiline\nmultiline").snap()

attest("with `quotes`").snap()

const it = (name: string, fn: () => void) => fn()

it("can snap instantiations", () => {
type Z = makeComplexType<"asbsdfsaodisfhsda">
attest.instantiations()
})

cleanup()
20 changes: 13 additions & 7 deletions ark/attest/bench/baseline.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { snapshot } from "@arktype/util"
import { AssertionError } from "node:assert"
import process from "node:process"
import {
queueSnapshotUpdate,
writeSnapshotUpdatesOnExit
} from "../cache/snapshots.js"
import type { BenchAssertionContext, BenchContext } from "./bench.js"
import type { BenchContext } from "./bench.js"
import {
stringifyMeasure,
type MarkMeasure,
Expand All @@ -15,7 +16,7 @@ import {
export const queueBaselineUpdateIfNeeded = (
updated: Measure | MarkMeasure,
baseline: Measure | MarkMeasure | undefined,
ctx: BenchAssertionContext
ctx: BenchContext
): void => {
// If we already have a baseline and the user didn't pass an update flag, do nothing
if (baseline && !ctx.cfg.updateSnapshots) return
Expand Down Expand Up @@ -61,11 +62,16 @@ const handlePositiveDelta = (formattedDelta: string, ctx: BenchContext) => {
const message = `'${ctx.qualifiedName}' exceeded baseline by ${formattedDelta} (threshold is ${ctx.cfg.benchPercentThreshold}%).`
console.error(`📈 ${message}`)
if (ctx.cfg.benchErrorOnThresholdExceeded) {
process.exitCode = 1
// Summarize failures at the end of output
process.on("exit", () => {
console.error(`❌ ${message}`)
})
const errorSummary = `❌ ${message}`
if (ctx.kind === "instantiations")
throw new AssertionError({ message: errorSummary })
else {
process.exitCode = 1
// Summarize failures at the end of output
process.on("exit", () => {
console.error(errorSummary)
})
}
}
}

Expand Down
3 changes: 0 additions & 3 deletions ark/attest/bench/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,6 @@ export type BenchContext = {
benchCallPosition: SourcePosition
lastSnapCallPosition: SourcePosition | undefined
isAsync: boolean
}

export type BenchAssertionContext = BenchContext & {
kind: TimeAssertionName | "types" | "instantiations"
}

Expand Down
4 changes: 2 additions & 2 deletions ark/attest/bench/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import type { TypeRelationship } from "../cache/writeAssertionCache.js"
import { getConfig } from "../config.js"
import { compareToBaseline, queueBaselineUpdateIfNeeded } from "./baseline.js"
import type { BenchAssertionContext, BenchContext } from "./bench.js"
import type { BenchContext } from "./bench.js"
import {
createTypeComparison,
type Measure,
Expand Down Expand Up @@ -75,7 +75,7 @@ export type ArgAssertionData = {
}

export const instantiationDataHandler = (
ctx: BenchAssertionContext,
ctx: BenchContext,
args?: Measure<TypeUnit>,
isBenchFunction = true
): void => {
Expand Down
2 changes: 1 addition & 1 deletion ark/attest/cache/writeAssertionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const getCompletions = (attestCall: ts.CallExpression) => {
}

return flatMorph(completions, (prefix, entries) =>
entries.length >= 1 ? [prefix, entries] : []
entries.length >= 1 ? [prefix, entries.sort()] : []
)
}

Expand Down
4 changes: 2 additions & 2 deletions ark/attest/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export const getDefaultAttestConfig = (): BaseAttestConfig => ({
skipInlineInstantiations: false,
tsVersions: "typescript",
benchPercentThreshold: 20,
benchErrorOnThresholdExceeded: false,
benchErrorOnThresholdExceeded: true,
filter: undefined,
testDeclarationAliases: ["bench", "it"],
testDeclarationAliases: ["bench", "it", "test"],
formatter: `npm exec --no -- prettier --write`,
shouldFormat: true
})
Expand Down
31 changes: 30 additions & 1 deletion ark/schema/__tests__/union.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { attest, contextualize } from "@arktype/attest"
import { schema, validation } from "@arktype/schema"
import {
schema,
validation,
writeOrderedIntersectionMessage
} from "@arktype/schema"

contextualize(() => {
it("binary", () => {
Expand Down Expand Up @@ -68,4 +72,29 @@ contextualize(() => {
const result = l.and(r)
attest(result.json).equals(l.json)
})

it("unordered union with ordered union", () => {
const l = schema({
branches: ["string", "number"],
ordered: true
})
const r = schema(["number", "string"])
const result = l.and(r)
attest(result.json).equals(l.json)
})

it("intersection of ordered unions", () => {
const l = schema({
branches: ["string", "number"],
ordered: true
})
const r = schema({
branches: ["number", "string"],
ordered: true
})

attest(() => l.and(r)).throws(
writeOrderedIntersectionMessage("string | number", "number | string")
)
})
})
2 changes: 1 addition & 1 deletion ark/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export * from "./shared/errors.js"
export * from "./shared/implement.js"
export * from "./shared/intersections.js"
export * from "./shared/utils.js"
export * from "./structure/index.js"
export * from "./structure/indexed.js"
export * from "./structure/optional.js"
export * from "./structure/prop.js"
export * from "./structure/sequence.js"
Expand Down
2 changes: 1 addition & 1 deletion ark/schema/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import {
IndexNode,
indexImplementation,
type IndexDeclaration
} from "./structure/index.js"
} from "./structure/indexed.js"
import {
OptionalNode,
optionalImplementation,
Expand Down
2 changes: 1 addition & 1 deletion ark/schema/refinements/before.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const beforeImplementation: nodeImplementationOf<BeforeDeclaration> =
before.overlapIsUnit(after) ?
ctx.$.node("unit", { unit: before.rule })
: null
: Disjoint.from("range", before, after)
: Disjoint.init("range", before, after)
}
})

Expand Down
18 changes: 8 additions & 10 deletions ark/schema/refinements/exactLength.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,30 +42,28 @@ export const exactLengthImplementation: nodeImplementationOf<ExactLengthDeclarat
},
intersections: {
exactLength: (l, r, ctx) =>
new Disjoint({
'["length"]': {
unit: {
l: ctx.$.node("unit", { unit: l.rule }),
r: ctx.$.node("unit", { unit: r.rule })
}
}
}),
Disjoint.init(
"unit",
ctx.$.node("unit", { unit: l.rule }),
ctx.$.node("unit", { unit: r.rule }),
{ path: ["length"] }
),
minLength: (exactLength, minLength) =>
(
minLength.exclusive ?
exactLength.rule > minLength.rule
: exactLength.rule >= minLength.rule
) ?
exactLength
: Disjoint.from("range", exactLength, minLength),
: Disjoint.init("range", exactLength, minLength),
maxLength: (exactLength, maxLength) =>
(
maxLength.exclusive ?
exactLength.rule < maxLength.rule
: exactLength.rule <= maxLength.rule
) ?
exactLength
: Disjoint.from("range", exactLength, maxLength)
: Disjoint.init("range", exactLength, maxLength)
}
})

Expand Down
4 changes: 2 additions & 2 deletions ark/schema/refinements/max.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
import type { TraverseAllows } from "../shared/traversal.js"
import {
BaseRange,
type BaseRangeInner,
parseExclusiveKey,
type BaseRangeInner,
type UnknownNormalizedRangeSchema
} from "./range.js"

Expand Down Expand Up @@ -55,7 +55,7 @@ export const maxImplementation: nodeImplementationOf<MaxDeclaration> =
max.overlapIsUnit(min) ?
ctx.$.node("unit", { unit: max.rule })
: null
: Disjoint.from("range", max, min)
: Disjoint.init("range", max, min)
}
})

Expand Down
4 changes: 2 additions & 2 deletions ark/schema/refinements/maxLength.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
import type { TraverseAllows } from "../shared/traversal.js"
import {
BaseRange,
parseExclusiveKey,
type BaseRangeInner,
type LengthBoundableData,
parseExclusiveKey,
type UnknownNormalizedRangeSchema
} from "./range.js"

Expand Down Expand Up @@ -60,7 +60,7 @@ export const maxLengthImplementation: nodeImplementationOf<MaxLengthDeclaration>
max.overlapIsUnit(min) ?
ctx.$.node("exactLength", { rule: max.rule })
: null
: Disjoint.from("range", max, min)
: Disjoint.init("range", max, min)
}
})

Expand Down
Loading

0 comments on commit 2be4f5b

Please sign in to comment.