Skip to content

Commit

Permalink
fix: strict/distilled array traversal, error on double right bound (a…
Browse files Browse the repository at this point in the history
  • Loading branch information
ShawnMorreau committed Apr 18, 2023
1 parent 6a4f403 commit d927a8e
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 0 deletions.
12 changes: 12 additions & 0 deletions dev/configs/.changeset/chilled-beans-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"arktype": patch
---

## add a syntax error when defining an expression with multiple right bounds

Ensures expressions like the following result in a syntax error during type validation (will currently not throw at runtime):

```ts
// Type Error: `Expression 'number' must have at most one right bound`
const boundedNumber = type("number>0<=200")
```
15 changes: 15 additions & 0 deletions dev/configs/.changeset/fast-chicken-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"arktype": patch
---

## fix array validation in strict and distilled modes

Previously, attempting to validate an array with "keys" set to "distilled" or "strict" would yield incorrect results.

Now, types like this behave as expected:

```ts
const strictArray = type("string[]", { keys: "strict" })
// data = ["foo", "bar"]
const { data, problems } = strictArray(["foo", "bar"])
```
31 changes: 31 additions & 0 deletions dev/test/keyTraversal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ describe("key traversal", () => {
attest(t({ a: "ok" }).data).equals({ a: "ok" })
attest(t(getExtraneousB()).data).snap({ a: "ok" })
})
it("distilled array", () => {
const o = type(
{ a: "email[]" },
{
keys: "distilled"
}
)
attest(o({ a: ["shawn@arktype.io"] }).data).snap({
a: ["shawn@arktype.io"]
})
attest(o({ a: ["notAnEmail"] }).problems?.summary).snap(
"a/0 must be a valid email (was 'notAnEmail')"
)
// can handle missing keys
attest(o({ b: ["shawn"] }).problems?.summary).snap("a must be defined")
})
it("distilled union", () => {
const o = type([{ a: "string" }, "|", { b: "boolean" }], {
keys: "distilled"
Expand Down Expand Up @@ -78,4 +94,19 @@ describe("key traversal", () => {
attest(t({ a: "ok" }).data).equals({ a: "ok" })
attest(t(getExtraneousB()).problems?.summary).snap("b must be removed")
})
it("strict array", () => {
const o = type(
{ a: "string[]" },
{
keys: "strict"
}
)
attest(o({ a: ["shawn"] }).data).snap({ a: ["shawn"] })
attest(o({ a: [2] }).problems?.summary).snap(
"a/0 must be a string (was number)"
)
attest(o({ b: ["shawn"] }).problems?.summary).snap(
"b must be removed\na must be defined"
)
})
})
7 changes: 7 additions & 0 deletions dev/test/range.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it } from "mocha"
import { type } from "../../src/main.js"
import type { ResolvedNode } from "../../src/nodes/node.js"
import { writeDoubleRightBoundMessage } from "../../src/parse/ast/bound.js"
import {
writeMultipleLeftBoundsMessage,
writeOpenRangeMessage,
Expand Down Expand Up @@ -210,6 +211,12 @@ describe("range", () => {
"Error: the range bounded by >=3 and <2 is empty"
)
})
it("double right bound", () => {
// @ts-expect-error
attest(() => type("number>0<=200")).type.errors(
writeDoubleRightBoundMessage("'number'")
)
})
})
})
})
11 changes: 11 additions & 0 deletions src/parse/ast/bound.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SizedData } from "../../utils/data.js"
import type { error, isAny } from "../../utils/generics.js"
import type { NumberLiteral } from "../../utils/numericLiterals.js"
import type { Scanner } from "../string/shift/scanner.js"
import type { inferAst, validateAst } from "./ast.js"
import type { astToString } from "./utils.js"

Expand All @@ -20,10 +21,20 @@ import type { astToString } from "./utils.js"
*/
export type validateBound<l, r, $> = l extends NumberLiteral
? validateAst<r, $>
: l extends [infer leftAst, Scanner.Comparator, unknown]
? error<writeDoubleRightBoundMessage<astToString<leftAst>>>
: isBoundable<inferAst<l, $>> extends true
? validateAst<l, $>
: error<writeUnboundableMessage<astToString<l>>>

export const writeDoubleRightBoundMessage = <root extends string>(
root: root
): writeDoubleRightBoundMessage<root> =>
`Expression ${root} must have at most one right bound`

type writeDoubleRightBoundMessage<root extends string> =
`Expression ${root} must have at most one right bound`

type isBoundable<data> = isAny<data> extends true
? false
: [data] extends [SizedData]
Expand Down
3 changes: 3 additions & 0 deletions src/traverse/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { domainOf, hasDomain } from "../utils/domains.js"
import { throwInternalError } from "../utils/errors.js"
import type { extend, stringKeyOf, xor } from "../utils/generics.js"
import { hasKey, objectKeysOf } from "../utils/generics.js"
import { wellFormedIntegerMatcher } from "../utils/numericLiterals.js"
import type { DefaultObjectKind } from "../utils/objectKinds.js"
import { getPath, Path } from "../utils/paths.js"
import type { ProblemCode, ProblemOptions, ProblemWriters } from "./problems.js"
Expand Down Expand Up @@ -245,6 +246,8 @@ const createPropChecker =
delete remainingUnseenRequired[k]
} else if (props.optional[k]) {
isValid = state.traverseKey(k, props.optional[k]) && isValid
} else if (props.index && wellFormedIntegerMatcher.test(k)) {
isValid = state.traverseKey(k, props.index) && isValid
} else if (kind === "distilledProps") {
if (state.failFast) {
// If we're in a union (i.e. failFast is enabled) in
Expand Down

0 comments on commit d927a8e

Please sign in to comment.