Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve arktype discrimination, attest instantiations and completions #1011

Merged
merged 12 commits into from
Jun 13, 2024
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