Skip to content

Commit 610e21e

Browse files
committed
fix: ensure coalesce handles optional/unknown/null properly
1 parent d94f686 commit 610e21e

File tree

3 files changed

+83
-54
lines changed

3 files changed

+83
-54
lines changed

src/typeEvaluator/functions.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable max-statements */
22
import type {FuncCallNode} from '../nodeTypes'
3+
import {optimizeUnions} from './optimizations'
34
import {Scope} from './scope'
45
import {walk} from './typeEvaluate'
56
import {mapNode, nullUnion} from './typeHelpers'
@@ -175,12 +176,32 @@ export function handleFuncCallNode(node: FuncCallNode, scope: Scope): TypeNode {
175176
const typeNodes: TypeNode[] = []
176177
let canBeNull = true
177178
for (const arg of node.args) {
178-
const type = walk({node: arg, scope})
179-
typeNodes.push(unionWithoutNull(type))
179+
const argNode = optimizeUnions(walk({node: arg, scope}))
180+
181+
// Check if all types are null
182+
const allNull =
183+
argNode.type === 'null' ||
184+
(argNode.type === 'union' && argNode.of.every((t) => t.type === 'null'))
185+
186+
// Can the argument be null, if all is null, unknown, or if its a union with at least one null or unknown
180187
canBeNull =
181-
type.type === 'null' || (type.type === 'union' && type.of.some((t) => t.type === 'null'))
188+
allNull ||
189+
argNode.type === 'unknown' ||
190+
(argNode.type === 'union' &&
191+
argNode.of.some((t) => t.type === 'null' || t.type === 'unknown'))
192+
193+
// As long as some type is not null or unknown, we add it to the union, but skip nulls
194+
if (!allNull) {
195+
typeNodes.push(unionWithoutNull(argNode))
196+
}
197+
198+
// If we have a type that can't be null, we can break.
199+
if (!canBeNull) {
200+
break
201+
}
182202
}
183203

204+
// If the last argument can be null, we add null to the union
184205
if (canBeNull) {
185206
typeNodes.push({type: 'null'} satisfies NullTypeNode)
186207
}

tap-snapshots/test/typeEvaluate.test.ts.test.cjs

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -257,54 +257,6 @@ Object {
257257
}
258258
`
259259

260-
exports[`test/typeEvaluate.test.ts TAP coalesce only > must match snapshot 1`] = `
261-
Object {
262-
"of": Object {
263-
"attributes": Object {
264-
"maybe": Object {
265-
"type": "objectAttribute",
266-
"value": Object {
267-
"of": Array [
268-
Object {
269-
"attributes": Object {
270-
"subfield": Object {
271-
"type": "objectAttribute",
272-
"value": Object {
273-
"type": "string",
274-
},
275-
},
276-
},
277-
"type": "object",
278-
},
279-
Object {
280-
"type": "null",
281-
},
282-
],
283-
"type": "union",
284-
},
285-
},
286-
"name": Object {
287-
"type": "objectAttribute",
288-
"value": Object {
289-
"of": Array [
290-
Object {
291-
"type": "string",
292-
},
293-
Object {
294-
"type": "string",
295-
"value": "unknown",
296-
},
297-
],
298-
"type": "union",
299-
},
300-
},
301-
},
302-
"type": "object",
303-
},
304-
"type": "array",
305-
}
306-
`
307-
308260
exports[`test/typeEvaluate.test.ts TAP coalesce with projection > must match snapshot 1`] = `
309261
Object {
310262
"of": Array [

test/typeEvaluate.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,12 +1623,68 @@ t.test('with conditional splat', (t) => {
16231623

16241624
t.test('coalesce only', async (t) => {
16251625
const query = `*[_type == "author"]{
1626-
"name": coalesce(name, "unknown"),
1627-
"maybe": coalesce(optionalObject, dontExists)
1626+
"name": coalesce(name, "unknown"), // should be string, since name is not optional
1627+
"maybe": coalesce(optionalObject, dontExists), // should be object or null
1628+
"multiple": coalesce(optionalAge, missing, "foo"), // should be either number or "foo"
1629+
"allMissing": coalesce(missing, missing2, missing3), // should be null, since all are missing
1630+
"fallback": coalesce(missing, missing2, missing3, "fallback"), // should be "fallback"
16281631
}`
16291632
const ast = parse(query)
16301633
const res = typeEvaluate(ast, schemas)
1631-
t.matchSnapshot(res)
1634+
t.strictSame(res, {
1635+
type: 'array',
1636+
of: {
1637+
type: 'object',
1638+
attributes: {
1639+
name: {
1640+
type: 'objectAttribute',
1641+
value: {
1642+
type: 'string',
1643+
},
1644+
},
1645+
maybe: {
1646+
type: 'objectAttribute',
1647+
value: {
1648+
type: 'union',
1649+
of: [
1650+
{
1651+
type: 'object',
1652+
attributes: {
1653+
subfield: {
1654+
type: 'objectAttribute',
1655+
value: {
1656+
type: 'string',
1657+
},
1658+
},
1659+
},
1660+
},
1661+
{type: 'null'},
1662+
],
1663+
},
1664+
},
1665+
multiple: {
1666+
type: 'objectAttribute',
1667+
value: {
1668+
type: 'union',
1669+
of: [{type: 'number'}, {type: 'string', value: 'foo'}],
1670+
},
1671+
},
1672+
allMissing: {
1673+
type: 'objectAttribute',
1674+
value: {
1675+
type: 'null',
1676+
},
1677+
},
1678+
fallback: {
1679+
type: 'objectAttribute',
1680+
value: {
1681+
type: 'string',
1682+
value: 'fallback',
1683+
},
1684+
},
1685+
},
1686+
},
1687+
} satisfies TypeNode)
16321688
t.end()
16331689
})
16341690

0 commit comments

Comments
 (0)