Comparing version 1.12.0 to 1.13.0
{ | ||
"name": "groq-js", | ||
"version": "1.12.0", | ||
"version": "1.13.0", | ||
"keywords": [ | ||
@@ -5,0 +5,0 @@ "sanity", |
@@ -122,3 +122,3 @@ # GROQ-JS<!-- omit in toc --> | ||
```shell | ||
GROQTEST_SUITE_VERSION=v0.1.33 ./test/generate.sh | ||
GROQTEST_SUITE_VERSION=v0.1.45 ./test/generate.sh | ||
``` | ||
@@ -125,0 +125,0 @@ |
@@ -438,3 +438,3 @@ import type {ExprNode} from '../nodeTypes' | ||
// All the document are a version of the given ID if: | ||
// 1. Document ID is of the ford bundleId.documentGroupId | ||
// 1. Document ID is of the form bundleId.documentGroupId | ||
// 2. And, they have a field called _version which is an object. | ||
@@ -462,2 +462,34 @@ const versionIds: string[] = [] | ||
// eslint-disable-next-line require-await | ||
sanity['documentsOf'] = async function (args, scope, execute) { | ||
if (!scope.source.isArray()) return NULL_VALUE | ||
const value = await execute(args[0], scope) | ||
if (value.type !== 'string') return NULL_VALUE | ||
const baseId = value.data | ||
// A document belongs to a bundle ID if: | ||
// 1. Document ID is of the form bundleId.documentGroupId | ||
// 2. And, they have a field called _version which is an object. | ||
const documentIdsInBundle: string[] = [] | ||
for await (const value of scope.source) { | ||
if (getType(value) === 'object') { | ||
const val = await value.get() | ||
if ( | ||
val && | ||
'_id' in val && | ||
val._id.split('.').length === 2 && | ||
val._id.startsWith(`${baseId}.`) && | ||
'_version' in val && | ||
typeof val._version === 'object' | ||
) { | ||
documentIdsInBundle.push(val._id) | ||
} | ||
} | ||
} | ||
return fromJS(documentIdsInBundle) | ||
} | ||
sanity['documentsOf'].arity = 1 | ||
export type GroqPipeFunction = ( | ||
@@ -464,0 +496,0 @@ base: Value, |
@@ -5,3 +5,3 @@ /* eslint-disable max-statements */ | ||
import {walk} from './typeEvaluate' | ||
import {mapConcrete, nullUnion} from './typeHelpers' | ||
import {mapNode, nullUnion} from './typeHelpers' | ||
import type {NullTypeNode, TypeNode} from './types' | ||
@@ -19,2 +19,3 @@ | ||
// eslint-disable-next-line complexity | ||
export function handleFuncCallNode(node: FuncCallNode, scope: Scope): TypeNode { | ||
@@ -25,3 +26,6 @@ switch (`${node.namespace}.${node.name}`) { | ||
return mapConcrete(arg, scope, (arg) => { | ||
return mapNode(arg, scope, (arg) => { | ||
if (arg.type === 'unknown') { | ||
return nullUnion({type: 'array', of: {type: 'unknown'}}) | ||
} | ||
if (arg.type !== 'array') { | ||
@@ -31,3 +35,3 @@ return {type: 'null'} | ||
const of = mapConcrete(arg.of, scope, (of) => of) | ||
const of = mapNode(arg.of, scope, (of) => of) | ||
return { | ||
@@ -44,15 +48,18 @@ type: 'array', | ||
return mapConcrete(arrayArg, scope, (arrayArg) => | ||
mapConcrete(sepArg, scope, (sepArg) => { | ||
if (arrayArg.type !== 'array') { | ||
return {type: 'null'} | ||
return mapNode(arrayArg, scope, (arrayArg) => | ||
mapNode(sepArg, scope, (sepArg) => { | ||
if (arrayArg.type === 'unknown' || sepArg.type === 'unknown') { | ||
return nullUnion({type: 'string'}) | ||
} | ||
if (sepArg.type !== 'string') { | ||
if (arrayArg.type !== 'array' || sepArg.type !== 'string') { | ||
return {type: 'null'} | ||
} | ||
return mapConcrete(arrayArg.of, scope, (of) => { | ||
return mapNode(arrayArg.of, scope, (of) => { | ||
if (of.type === 'unknown') { | ||
return nullUnion({type: 'string'}) | ||
} | ||
// we can only join strings, numbers, and booleans | ||
if (of.type !== 'string' && of.type !== 'number' && of.type !== 'boolean') { | ||
return {type: 'unknown'} | ||
return {type: 'null'} | ||
} | ||
@@ -69,3 +76,6 @@ | ||
return mapConcrete(arg, scope, (arg) => { | ||
return mapNode(arg, scope, (arg) => { | ||
if (arg.type === 'unknown') { | ||
return nullUnion({type: 'array', of: {type: 'unknown'}}) | ||
} | ||
if (arg.type !== 'array') { | ||
@@ -82,14 +92,17 @@ return {type: 'null'} | ||
return mapConcrete(arg, scope, (arg) => { | ||
if (arg.type === 'string') { | ||
if (arg.value !== undefined) { | ||
return { | ||
type: 'string', | ||
value: arg.value.toLowerCase(), | ||
} | ||
return mapNode(arg, scope, (arg) => { | ||
if (arg.type === 'unknown') { | ||
return nullUnion({type: 'string'}) | ||
} | ||
if (arg.type !== 'string') { | ||
return {type: 'null'} | ||
} | ||
if (arg.value !== undefined) { | ||
return { | ||
type: 'string', | ||
value: arg.value.toLowerCase(), | ||
} | ||
return {type: 'string'} | ||
} | ||
return {type: 'null'} | ||
return {type: 'string'} | ||
}) | ||
@@ -100,14 +113,16 @@ } | ||
return mapConcrete(arg, scope, (arg) => { | ||
if (arg.type === 'string') { | ||
if (arg.value !== undefined) { | ||
return { | ||
type: 'string', | ||
value: arg.value.toUpperCase(), | ||
} | ||
return mapNode(arg, scope, (arg) => { | ||
if (arg.type === 'unknown') { | ||
return nullUnion({type: 'string'}) | ||
} | ||
if (arg.type !== 'string') { | ||
return {type: 'null'} | ||
} | ||
if (arg.value !== undefined) { | ||
return { | ||
type: 'string', | ||
value: arg.value.toUpperCase(), | ||
} | ||
return {type: 'string'} | ||
} | ||
return {type: 'null'} | ||
return {type: 'string'} | ||
}) | ||
@@ -124,2 +139,16 @@ } | ||
} | ||
case 'global.path': { | ||
const arg = walk({node: node.args[0], scope}) | ||
return mapNode(arg, scope, (arg) => { | ||
if (arg.type === 'unknown') { | ||
return nullUnion({type: 'string'}) | ||
} | ||
if (arg.type === 'string') { | ||
return {type: 'string'} | ||
} | ||
return {type: 'null'} | ||
}) | ||
} | ||
case 'global.coalesce': { | ||
@@ -151,3 +180,7 @@ if (node.args.length === 0) { | ||
return mapConcrete(arg, scope, (arg) => { | ||
return mapNode(arg, scope, (arg) => { | ||
if (arg.type === 'unknown') { | ||
return nullUnion({type: 'string'}) | ||
} | ||
if (arg.type === 'array') { | ||
@@ -164,3 +197,7 @@ return {type: 'number'} | ||
return mapConcrete(arg, scope, (arg) => { | ||
return mapNode(arg, scope, (arg) => { | ||
if (arg.type === 'unknown') { | ||
return nullUnion({type: 'string'}) | ||
} | ||
if (arg.type === 'string') { | ||
@@ -177,8 +214,7 @@ return nullUnion({type: 'string'}) // we don't know wether the string is a valid date or not, so we return a [null, string]-union | ||
return mapConcrete(arg, scope, (arg) => { | ||
if (arg.type === 'array') { | ||
return {type: 'number'} | ||
return mapNode(arg, scope, (arg) => { | ||
if (arg.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (arg.type === 'string') { | ||
if (arg.type === 'array' || arg.type === 'string') { | ||
return {type: 'number'} | ||
@@ -198,3 +234,7 @@ } | ||
return mapConcrete(numNode, scope, (num) => { | ||
return mapNode(numNode, scope, (num) => { | ||
if (num.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (num.type !== 'number') { | ||
@@ -205,3 +245,7 @@ return {type: 'null'} | ||
const precisionNode = walk({node: node.args[1], scope}) | ||
return mapConcrete(precisionNode, scope, (precision) => { | ||
return mapNode(precisionNode, scope, (precision) => { | ||
if (precision.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (precision.type !== 'number') { | ||
@@ -221,3 +265,7 @@ return {type: 'null'} | ||
const arg = walk({node: node.args[0], scope}) | ||
return mapConcrete(arg, scope, (node) => { | ||
return mapNode(arg, scope, (node) => { | ||
if (node.type === 'unknown') { | ||
return nullUnion({type: 'string'}) | ||
} | ||
if (node.type === 'string' || node.type === 'number' || node.type === 'boolean') { | ||
@@ -242,17 +290,25 @@ if (node.value) { | ||
const values = walk({node: node.args[0], scope}) | ||
// use mapConcrete to get concrete resolved value, it will also handle cases where the value is a union | ||
return mapConcrete(values, scope, (node) => { | ||
// use mapNode to get concrete resolved value, it will also handle cases where the value is a union | ||
return mapNode(values, scope, (node) => { | ||
if (node.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
// Aggregate functions can only be applied to arrays | ||
if (node.type === 'array') { | ||
// Resolve the concrete type of the array elements | ||
return mapConcrete(node.of, scope, (node) => { | ||
// Math functions can only be applied to numbers, but we should also ignore nulls | ||
if (node.type === 'number' || node.type === 'null') { | ||
return {type: 'number'} | ||
} | ||
return {type: 'null'} | ||
}) | ||
if (node.type !== 'array') { | ||
return {type: 'null'} | ||
} | ||
return {type: 'null'} | ||
// Resolve the concrete type of the array elements | ||
return mapNode(node.of, scope, (node) => { | ||
if (node.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
// Math functions can only be applied to numbers, but we should also ignore nulls | ||
if (node.type === 'number' || node.type === 'null') { | ||
return {type: 'number'} | ||
} | ||
return {type: 'null'} | ||
}) | ||
}) | ||
@@ -263,17 +319,24 @@ } | ||
const values = walk({node: node.args[0], scope}) | ||
// use mapConcrete to get concrete resolved value, it will also handle cases where the value is a union | ||
return mapConcrete(values, scope, (node) => { | ||
// use mapNode to get concrete resolved value, it will also handle cases where the value is a union | ||
return mapNode(values, scope, (node) => { | ||
if (node.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
// Aggregate functions can only be applied to arrays | ||
if (node.type === 'array') { | ||
// Resolve the concrete type of the array elements | ||
return mapConcrete(node.of, scope, (node) => { | ||
// Math functions can only be applied to numbers | ||
if (node.type === 'number') { | ||
return {type: 'number'} | ||
} | ||
return {type: 'null'} | ||
}) | ||
if (node.type !== 'array') { | ||
return {type: 'null'} | ||
} | ||
// Resolve the concrete type of the array elements | ||
return mapNode(node.of, scope, (node) => { | ||
if (node.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
return {type: 'null'} | ||
// Math functions can only be applied to numbers | ||
if (node.type === 'number') { | ||
return {type: 'number'} | ||
} | ||
return {type: 'null'} | ||
}) | ||
}) | ||
@@ -285,17 +348,25 @@ } | ||
const values = walk({node: node.args[0], scope}) | ||
// use mapConcrete to get concrete resolved value, it will also handle cases where the value is a union | ||
return mapConcrete(values, scope, (node) => { | ||
// use mapNode to get concrete resolved value, it will also handle cases where the value is a union | ||
return mapNode(values, scope, (node) => { | ||
if (node.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
// Aggregate functions can only be applied to arrays | ||
if (node.type === 'array') { | ||
// Resolve the concrete type of the array elements | ||
return mapConcrete(node.of, scope, (node) => { | ||
// Math functions can only be applied to numbers | ||
if (node.type === 'number') { | ||
return node | ||
} | ||
return {type: 'null'} | ||
}) | ||
if (node.type !== 'array') { | ||
return {type: 'null'} | ||
} | ||
return {type: 'null'} | ||
// Resolve the concrete type of the array elements | ||
return mapNode(node.of, scope, (node) => { | ||
if (node.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
// Math functions can only be applied to numbers | ||
if (node.type === 'number') { | ||
return node | ||
} | ||
return {type: 'null'} | ||
}) | ||
}) | ||
@@ -312,12 +383,13 @@ } | ||
} | ||
case 'string.startsWith': { | ||
const strTypeNode = walk({node: node.args[0], scope}) | ||
const prefixTypeNode = walk({node: node.args[1], scope}) | ||
return mapConcrete(strTypeNode, scope, (strNode) => { | ||
if (strNode.type !== 'string') { | ||
return {type: 'null'} | ||
} | ||
return mapNode(strTypeNode, scope, (strNode) => { | ||
return mapNode(prefixTypeNode, scope, (prefixNode) => { | ||
if (strNode.type === 'unknown' || prefixNode.type === 'unknown') { | ||
return nullUnion({type: 'boolean'}) | ||
} | ||
return mapConcrete(prefixTypeNode, scope, (prefixNode) => { | ||
if (prefixNode.type !== 'string') { | ||
if (strNode.type !== 'string' || prefixNode.type !== 'string') { | ||
return {type: 'null'} | ||
@@ -333,9 +405,9 @@ } | ||
const sepTypeNode = walk({node: node.args[1], scope}) | ||
return mapConcrete(strTypeNode, scope, (strNode) => { | ||
if (strNode.type !== 'string') { | ||
return {type: 'null'} | ||
} | ||
return mapNode(strTypeNode, scope, (strNode) => { | ||
return mapNode(sepTypeNode, scope, (sepNode) => { | ||
if (strNode.type === 'unknown' || sepNode.type === 'unknown') { | ||
return nullUnion({type: 'array', of: {type: 'string'}}) | ||
} | ||
return mapConcrete(sepTypeNode, scope, (sepNode) => { | ||
if (sepNode.type !== 'string') { | ||
if (strNode.type !== 'string' || sepNode.type !== 'string') { | ||
return {type: 'null'} | ||
@@ -350,3 +422,6 @@ } | ||
const typeNode = walk({node: node.args[0], scope}) | ||
return mapConcrete(typeNode, scope, (typeNode) => { | ||
return mapNode(typeNode, scope, (typeNode) => { | ||
if (typeNode.type === 'unknown') { | ||
return nullUnion({type: 'array', of: {type: 'string'}}) | ||
} | ||
if (typeNode.type !== 'string') { | ||
@@ -358,2 +433,15 @@ return {type: 'null'} | ||
} | ||
case 'sanity.documentsOf': { | ||
const typeNode = walk({node: node.args[0], scope}) | ||
return mapNode(typeNode, scope, (typeNode) => { | ||
if (typeNode.type === 'unknown') { | ||
return nullUnion({type: 'array', of: {type: 'string'}}) | ||
} | ||
if (typeNode.type !== 'string') { | ||
return {type: 'null'} | ||
} | ||
return {type: 'array', of: {type: 'string'}} | ||
}) | ||
} | ||
default: { | ||
@@ -360,0 +448,0 @@ return {type: 'unknown'} |
import debug from 'debug' | ||
import { | ||
matchAnalyzePattern, | ||
matchText, | ||
matchTokenize, | ||
type Pattern, | ||
type Token, | ||
} from '../evaluator/matching' | ||
import type { | ||
AccessAttributeNode, | ||
AccessElementNode, | ||
AndNode, | ||
ArrayCoerceNode, | ||
@@ -26,3 +20,5 @@ ArrayNode, | ||
ObjectSplatNode, | ||
OpCall, | ||
OpCallNode, | ||
OrNode, | ||
ParentNode, | ||
@@ -35,5 +31,8 @@ PosNode, | ||
} from '../nodeTypes' | ||
import {booleanAnd, booleanInterpretationToTypeNode, booleanOr, booleanValue} from './booleans' | ||
import {handleFuncCallNode} from './functions' | ||
import {match} from './matching' | ||
import {optimizeUnions} from './optimizations' | ||
import {Context, Scope} from './scope' | ||
import {isFuncCall, mapNode, nullUnion, resolveInline} from './typeHelpers' | ||
import type { | ||
@@ -54,3 +53,2 @@ ArrayTypeNode, | ||
} from './types' | ||
import {mapConcrete, nullUnion, resolveInline} from './typeHelpers' | ||
@@ -131,3 +129,7 @@ const $trace = debug('typeEvaluator:evaluate:trace') | ||
$trace('object.splat.value %O', value) | ||
return mapConcrete(value, scope, (node) => { | ||
return mapNode(value, scope, (node) => { | ||
// splatting over unknown is unknown, we can't know what the attributes are | ||
if (node.type === 'unknown') { | ||
return {type: 'unknown'} | ||
} | ||
// splatting over a non-object is a no-op | ||
@@ -230,6 +232,6 @@ if (node.type !== 'object') { | ||
if (attr.type === 'ObjectConditionalSplat') { | ||
const condition = resolveCondition(attr.condition, scope) | ||
const condition = booleanValue(walk({node: attr.condition, scope}), scope) | ||
$trace('object.conditional.splat.condition %O', condition) | ||
// condition is never met, skip this attribute | ||
if (condition === false) { | ||
if (condition.canBeTrue === false) { | ||
continue | ||
@@ -241,3 +243,3 @@ } | ||
// condition is always met, we can treat this as a normal splat | ||
if (condition === true) { | ||
if (condition.canBeFalse === false && condition.canBeNull === false) { | ||
switch (attributeNode.type) { | ||
@@ -265,3 +267,3 @@ case 'object': { | ||
const variant = mapConcrete(attributeNode, scope, (attributeNode) => { | ||
const variant = mapNode(attributeNode, scope, (attributeNode) => { | ||
$trace('object.conditional.splat.result.concrete %O', attributeNode) | ||
@@ -470,13 +472,65 @@ if (attributeNode.type !== 'object') { | ||
const rhs = walk({node: node.right, scope}) | ||
return mapConcrete(lhs, scope, (left) => | ||
// eslint-disable-next-line complexity | ||
mapConcrete(rhs, scope, (right) => { | ||
return mapNode(lhs, scope, (left) => | ||
// eslint-disable-next-line complexity, max-statements | ||
mapNode(rhs, scope, (right) => { | ||
$trace('opcall.node.concrete "%s" %O', node.op, {left, right}) | ||
switch (node.op) { | ||
case '==': | ||
case '==': { | ||
// == always returns a boolean, no matter the compared types. | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return {type: 'boolean'} | ||
} | ||
if (left.type !== right.type) { | ||
return { | ||
type: 'boolean', | ||
value: false, | ||
} satisfies BooleanTypeNode | ||
} | ||
if (left.type === 'null') { | ||
return { | ||
type: 'boolean', | ||
value: true, | ||
} satisfies BooleanTypeNode | ||
} | ||
if (!isPrimitiveTypeNode(left) || !isPrimitiveTypeNode(right)) { | ||
return { | ||
type: 'boolean', | ||
value: false, | ||
} satisfies BooleanTypeNode | ||
} | ||
return { | ||
type: 'boolean', | ||
value: evaluateComparison(node.op, left, right), | ||
} satisfies BooleanTypeNode | ||
} | ||
case '!=': { | ||
// != always returns a boolean, no matter the compared types. | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return {type: 'boolean'} | ||
} | ||
if (left.type !== right.type) { | ||
return { | ||
type: 'boolean', | ||
value: true, | ||
} satisfies BooleanTypeNode | ||
} | ||
if (left.type === 'null') { | ||
return { | ||
type: 'boolean', | ||
value: false, | ||
} satisfies BooleanTypeNode | ||
} | ||
if (!isPrimitiveTypeNode(left) || !isPrimitiveTypeNode(right)) { | ||
return { | ||
type: 'boolean', | ||
value: true, | ||
} satisfies BooleanTypeNode | ||
} | ||
let value = evaluateComparison('==', left, right) | ||
if (value !== undefined) value = !value | ||
return { | ||
type: 'boolean', | ||
value: resolveCondition(node, scope), | ||
value, | ||
} satisfies BooleanTypeNode | ||
@@ -488,34 +542,84 @@ } | ||
case '<=': { | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return nullUnion({type: 'boolean'}) | ||
} | ||
if (left.type !== right.type) { | ||
return {type: 'null'} satisfies NullTypeNode | ||
} | ||
if (!isPrimitiveTypeNode(left) || !isPrimitiveTypeNode(right)) { | ||
return {type: 'null'} satisfies NullTypeNode | ||
} | ||
return { | ||
type: 'boolean', | ||
value: evaluateComparison(node.op, left, right), | ||
} satisfies BooleanTypeNode | ||
} | ||
case 'in': { | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return nullUnion({type: 'boolean'}) | ||
} | ||
if (right.type !== 'array') { | ||
// Special case for global::path, since it can be used with in operator, but the type returned otherwise is a string | ||
if (isFuncCall(node.right, 'global::path')) { | ||
return {type: 'boolean'} | ||
} | ||
return {type: 'null'} | ||
} | ||
if (isPrimitiveTypeNode(left)) { | ||
const resolved = resolveCondition(node, scope) | ||
if (!isPrimitiveTypeNode(left) && left.type !== 'null') { | ||
return { | ||
type: 'boolean', | ||
value: resolved, | ||
value: false, | ||
} satisfies BooleanTypeNode | ||
} | ||
return mapNode(right.of, scope, (arrayTypeNode) => { | ||
if (arrayTypeNode.type === 'unknown') { | ||
return nullUnion({type: 'boolean'}) | ||
} | ||
return {type: 'null'} | ||
} | ||
case 'in': { | ||
if (right.type === 'array') { | ||
const resolved = resolveCondition(node, scope) | ||
if (left.type === 'null') { | ||
return { | ||
type: 'boolean', | ||
value: arrayTypeNode.type === 'null', | ||
} satisfies BooleanTypeNode | ||
} | ||
if (left.value === undefined) { | ||
return { | ||
type: 'boolean', | ||
} satisfies BooleanTypeNode | ||
} | ||
if (isPrimitiveTypeNode(arrayTypeNode)) { | ||
if (arrayTypeNode.value === undefined) { | ||
return { | ||
type: 'boolean', | ||
} satisfies BooleanTypeNode | ||
} | ||
return { | ||
type: 'boolean', | ||
value: left.value === arrayTypeNode.value, | ||
} satisfies BooleanTypeNode | ||
} | ||
return { | ||
type: 'boolean', | ||
value: resolved, | ||
value: false, | ||
} satisfies BooleanTypeNode | ||
} | ||
return {type: 'null'} | ||
}) | ||
} | ||
case 'match': { | ||
const resolved = resolveCondition(node, scope) | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
// match always returns a boolean, no matter the compared types. | ||
return {type: 'boolean'} | ||
} | ||
return { | ||
type: 'boolean', | ||
value: resolved, | ||
value: match(left, right), | ||
} satisfies BooleanTypeNode | ||
} | ||
case '+': { | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
// + is ambiguous without the concrete types of the operands, so we return unknown and leave the excersise to the caller | ||
return {type: 'unknown'} | ||
} | ||
if (left.type === 'string' && right.type === 'string') { | ||
@@ -558,2 +662,5 @@ return { | ||
case '-': { | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (left.type === 'number' && right.type === 'number') { | ||
@@ -571,2 +678,5 @@ return { | ||
case '*': { | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (left.type === 'number' && right.type === 'number') { | ||
@@ -584,2 +694,5 @@ return { | ||
case '/': { | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (left.type === 'number' && right.type === 'number') { | ||
@@ -597,2 +710,5 @@ return { | ||
case '**': { | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (left.type === 'number' && right.type === 'number') { | ||
@@ -610,2 +726,5 @@ return { | ||
case '%': { | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (left.type === 'number' && right.type === 'number') { | ||
@@ -623,2 +742,5 @@ return { | ||
default: { | ||
// TS only: make sure we handle all cases | ||
node.op satisfies never | ||
return { | ||
@@ -669,3 +791,3 @@ type: 'unknown', | ||
return mapConcrete( | ||
return mapNode( | ||
inner, | ||
@@ -732,3 +854,3 @@ scope, | ||
return mapConcrete(base, scope, (base) => { | ||
return mapNode(base, scope, (base) => { | ||
$trace('filter.resolving %O', base) | ||
@@ -886,24 +1008,45 @@ if (base.type === 'null') { | ||
const base = walk({node: node.base, scope}) | ||
if (base.type === 'boolean' && base.value !== undefined) { | ||
return {type: 'boolean', value: base.value === false} | ||
} | ||
return {type: 'boolean'} | ||
return mapNode(base, scope, (base) => { | ||
if (base.type === 'unknown') { | ||
return nullUnion({type: 'boolean'}) | ||
} | ||
if (base.type === 'boolean') { | ||
if (base.value !== undefined) { | ||
return {type: 'boolean', value: base.value === false} | ||
} | ||
return {type: 'boolean'} | ||
} | ||
return {type: 'null'} | ||
}) | ||
} | ||
function handleNegNode(node: NegNode, scope: Scope): NumberTypeNode | NullTypeNode { | ||
function handleNegNode(node: NegNode, scope: Scope): TypeNode { | ||
const base = walk({node: node.base, scope}) | ||
if (base.type !== 'number') { | ||
return {type: 'null'} | ||
} | ||
if (base.value !== undefined) { | ||
return {type: 'number', value: -base.value} | ||
} | ||
return base | ||
return mapNode(base, scope, (base) => { | ||
if (base.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (base.type !== 'number') { | ||
return {type: 'null'} | ||
} | ||
if (base.value !== undefined) { | ||
return {type: 'number', value: -base.value} | ||
} | ||
return base | ||
}) | ||
} | ||
function handlePosNode(node: PosNode, scope: Scope): NumberTypeNode | NullTypeNode { | ||
function handlePosNode(node: PosNode, scope: Scope): TypeNode { | ||
const base = walk({node: node.base, scope}) | ||
if (base.type !== 'number') { | ||
return {type: 'null'} | ||
} | ||
return base | ||
return mapNode(base, scope, (base) => { | ||
if (base.type === 'unknown') { | ||
return nullUnion({type: 'number'}) | ||
} | ||
if (base.type !== 'number') { | ||
return {type: 'null'} | ||
} | ||
return base | ||
}) | ||
} | ||
@@ -926,2 +1069,26 @@ | ||
function handleAndNode(node: AndNode, scope: Scope): TypeNode { | ||
const left = walk({node: node.left, scope}) | ||
const right = walk({node: node.right, scope}) | ||
return mapNode(left, scope, (lhs) => | ||
mapNode(right, scope, (rhs) => { | ||
const value = booleanAnd(booleanValue(lhs, scope), booleanValue(rhs, scope)) | ||
return booleanInterpretationToTypeNode(value) | ||
}), | ||
) | ||
} | ||
function handleOrNode(node: OrNode, scope: Scope): TypeNode { | ||
const left = walk({node: node.left, scope}) | ||
const right = walk({node: node.right, scope}) | ||
return mapNode(left, scope, (lhs) => | ||
mapNode(right, scope, (rhs) => { | ||
const value = booleanOr(booleanValue(lhs, scope), booleanValue(rhs, scope)) | ||
return booleanInterpretationToTypeNode(value) | ||
}), | ||
) | ||
} | ||
const OVERRIDE_TYPE_SYMBOL = Symbol('groq-js.type') | ||
@@ -982,8 +1149,8 @@ | ||
case 'And': | ||
case 'And': { | ||
return handleAndNode(node, scope) | ||
} | ||
case 'Or': { | ||
return { | ||
type: 'boolean', | ||
value: resolveCondition(node, scope), | ||
} satisfies BooleanTypeNode | ||
return handleOrNode(node, scope) | ||
} | ||
@@ -1070,299 +1237,28 @@ | ||
function evaluateEquality(left: TypeNode, right: TypeNode): boolean | undefined { | ||
$trace('evaluateEquality %O', {left, right}) | ||
if (left.type === 'null' && right.type === 'null') { | ||
return true | ||
function evaluateComparison( | ||
opcall: OpCall, | ||
left: PrimitiveTypeNode, | ||
right: PrimitiveTypeNode, | ||
): boolean | undefined { | ||
if (left.value === undefined || right.value === undefined) { | ||
return undefined | ||
} | ||
if ( | ||
isPrimitiveTypeNode(left) && | ||
isPrimitiveTypeNode(right) && | ||
left.value !== undefined && | ||
right.value !== undefined | ||
) { | ||
return left.value === right.value | ||
} | ||
if (left.type === 'union' && isPrimitiveTypeNode(right)) { | ||
for (const node of left.of) { | ||
// both are primitive types, and their values are equal, we can return true | ||
if (isPrimitiveTypeNode(node) && node.value === right.value) { | ||
return true | ||
} | ||
// both are the same type, but the value is undefined, we can't determine the result | ||
if (isPrimitiveTypeNode(node) && node.value === undefined) { | ||
return undefined | ||
} | ||
switch (opcall) { | ||
case '==': { | ||
return left.value === right.value | ||
} | ||
} | ||
if (left.type !== right.type) { | ||
return false | ||
} | ||
return undefined | ||
} | ||
/** | ||
* Resolves the condition expression and returns a boolean value or undefined. | ||
* Undefined is returned when the condition can't be resolved. | ||
* | ||
* @param expr - The expression node to resolve. | ||
* @param scope - The scope in which the expression is evaluated. | ||
* @returns The resolved boolean value or undefined. | ||
*/ | ||
// eslint-disable-next-line complexity, max-statements | ||
function resolveCondition(expr: ExprNode, scope: Scope): boolean | undefined { | ||
$trace('resolveCondition.expr %O', expr) | ||
switch (expr.type) { | ||
case 'AccessAttribute': | ||
case 'AccessElement': | ||
case 'Value': { | ||
const value = mapConcrete(walk({node: expr, scope}), scope, (node) => node) | ||
if (value.type === 'boolean') { | ||
return value.value | ||
} | ||
if (value.type === 'null' || value.type === 'object' || value.type === 'array') { | ||
return false | ||
} | ||
return undefined | ||
case '<': { | ||
return left.value < right.value | ||
} | ||
case 'And': { | ||
const left = resolveCondition(expr.left, scope) | ||
$trace('resolveCondition.and.left %O', left) | ||
if (left === false) { | ||
return false | ||
} | ||
const right = resolveCondition(expr.right, scope) | ||
$trace('resolveCondition.and.right %O', right) | ||
if (right === false) { | ||
return false | ||
} | ||
if (left === undefined || right === undefined) { | ||
return undefined | ||
} | ||
return true | ||
case '<=': { | ||
return left.value <= right.value | ||
} | ||
case 'Or': { | ||
$trace('resolveCondition.or.expr %O', expr) | ||
const left = resolveCondition(expr.left, scope) | ||
$trace('resolveCondition.or.left %O', left) | ||
if (left === true) { | ||
return true | ||
} | ||
const right = resolveCondition(expr.right, scope) | ||
$trace('resolveCondition.or.right %O', right) | ||
if (right === true) { | ||
return true | ||
} | ||
if (left === undefined || right === undefined) { | ||
return undefined | ||
} | ||
return false | ||
case '>': { | ||
return left.value > right.value | ||
} | ||
case 'OpCall': { | ||
const left = walk({node: expr.left, scope}) | ||
const right = walk({node: expr.right, scope}) | ||
$trace('opcall "%s" %O', expr.op, {left, right}) | ||
if (left.type === 'unknown' || right.type === 'unknown') { | ||
return undefined | ||
} | ||
switch (expr.op) { | ||
case '==': { | ||
return evaluateEquality(left, right) | ||
} | ||
case '!=': { | ||
const result = evaluateEquality(left, right) | ||
if (result === undefined) { | ||
return undefined | ||
} | ||
return !result | ||
} | ||
case 'in': { | ||
if (right.type === 'array') { | ||
if (left.type === 'null' && right.of.type === 'unknown') { | ||
return undefined | ||
} | ||
if (left.type === 'null' && right.of.type === 'null') { | ||
return true | ||
} | ||
if (isPrimitiveTypeNode(left)) { | ||
// eslint-disable-next-line max-depth | ||
if (right.of.type === 'unknown') { | ||
return undefined | ||
} | ||
// eslint-disable-next-line max-depth | ||
if (left.value === undefined) { | ||
return undefined | ||
} | ||
// eslint-disable-next-line max-depth | ||
if (isPrimitiveTypeNode(right.of)) { | ||
// eslint-disable-next-line max-depth | ||
if (right.of.value === undefined) { | ||
return undefined | ||
} | ||
return left.value === right.of.value | ||
} | ||
// eslint-disable-next-line max-depth | ||
if (right.of.type === 'union') { | ||
// eslint-disable-next-line max-depth | ||
for (const node of right.of.of) { | ||
// eslint-disable-next-line max-depth | ||
if (node.type === 'unknown') { | ||
return undefined | ||
} | ||
// eslint-disable-next-line max-depth | ||
if (isPrimitiveTypeNode(node) && left.value === node.value) { | ||
return true | ||
} | ||
// eslint-disable-next-line max-depth | ||
if (left.type === node.type && node.value === undefined) { | ||
return undefined | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
case 'match': { | ||
let tokens: Token[] = [] | ||
let patterns: Pattern[] = [] | ||
if (left.type === 'string') { | ||
if (left.value === undefined) { | ||
return undefined | ||
} | ||
tokens = tokens.concat(matchTokenize(left.value)) | ||
} | ||
if (left.type === 'array') { | ||
if (left.of.type === 'unknown') { | ||
return undefined | ||
} | ||
if (left.of.type === 'string') { | ||
// eslint-disable-next-line max-depth | ||
if (left.of.value === undefined) { | ||
return undefined | ||
} | ||
tokens = tokens.concat(matchTokenize(left.of.value)) | ||
} | ||
if (left.of.type === 'union') { | ||
// eslint-disable-next-line max-depth | ||
for (const node of left.of.of) { | ||
// eslint-disable-next-line max-depth | ||
if (node.type === 'string' && node.value !== undefined) { | ||
tokens = tokens.concat(matchTokenize(node.value)) | ||
} | ||
} | ||
} | ||
} | ||
if (right.type === 'string') { | ||
if (right.value === undefined) { | ||
return undefined | ||
} | ||
patterns = patterns.concat(matchAnalyzePattern(right.value)) | ||
} | ||
if (right.type === 'array') { | ||
if (right.of.type === 'unknown') { | ||
return undefined | ||
} | ||
if (right.of.type === 'string') { | ||
// eslint-disable-next-line max-depth | ||
if (right.of.value === undefined) { | ||
return undefined | ||
} | ||
patterns = patterns.concat(matchAnalyzePattern(right.of.value)) | ||
} | ||
if (right.of.type === 'union') { | ||
// eslint-disable-next-line max-depth | ||
for (const node of right.of.of) { | ||
// eslint-disable-next-line max-depth | ||
if (node.type === 'string') { | ||
// eslint-disable-next-line max-depth | ||
if (node.value === undefined) { | ||
return undefined | ||
} | ||
patterns = patterns.concat(matchAnalyzePattern(node.value)) | ||
} | ||
// eslint-disable-next-line max-depth | ||
if (node.type !== 'string') { | ||
return false | ||
} | ||
} | ||
} | ||
} | ||
return matchText(tokens, patterns) | ||
} | ||
case '<': { | ||
if (isPrimitiveTypeNode(left) && isPrimitiveTypeNode(right)) { | ||
if (left.value === undefined || right.value === undefined) { | ||
return undefined | ||
} | ||
return left.value < right.value | ||
} | ||
return undefined | ||
} | ||
case '<=': { | ||
if (isPrimitiveTypeNode(left) && isPrimitiveTypeNode(right)) { | ||
if (left.value === undefined || right.value === undefined) { | ||
return undefined | ||
} | ||
return left.value <= right.value | ||
} | ||
return undefined | ||
} | ||
case '>': { | ||
if (isPrimitiveTypeNode(left) && isPrimitiveTypeNode(right)) { | ||
if (left.value === undefined || right.value === undefined) { | ||
return undefined | ||
} | ||
return left.value > right.value | ||
} | ||
return undefined | ||
} | ||
case '>=': { | ||
if (isPrimitiveTypeNode(left) && isPrimitiveTypeNode(right)) { | ||
if (left.value === undefined || right.value === undefined) { | ||
return undefined | ||
} | ||
return left.value >= right.value | ||
} | ||
return undefined | ||
} | ||
default: { | ||
return undefined | ||
} | ||
} | ||
case '>=': { | ||
return left.value >= right.value | ||
} | ||
case 'Not': { | ||
const result = resolveCondition(expr.base, scope) | ||
// check if the result is undefined or false. Undefined means that the condition can't be resolved, and we should keep the node | ||
return result === undefined ? undefined : result === false | ||
} | ||
case 'Group': { | ||
return resolveCondition(expr.base, scope) | ||
} | ||
default: { | ||
return undefined | ||
throw new Error(`unknown comparison operator ${opcall}`) | ||
} | ||
@@ -1375,8 +1271,9 @@ } | ||
$trace('resolveFilter.expr %O', expr) | ||
const filtered = scope.value.of.filter( | ||
(node) => | ||
// create a new scope with the current scopes parent as the parent. It's only a temporary scope since we only want to resolve the condition | ||
// check if the result is true or undefined. Undefined means that the condition can't be resolved, and we should keep the node | ||
resolveCondition(expr, scope.createHidden([node])) !== false, | ||
) | ||
const filtered = scope.value.of.filter((node) => { | ||
// create a new scope with the current scopes parent as the parent. It's only a temporary scope since we only want to resolve the condition | ||
// and check if the result can be true. | ||
const subScope = scope.createHidden([node]) | ||
const cond = walk({node: expr, scope: subScope}) | ||
return booleanValue(cond, subScope).canBeTrue | ||
}) | ||
$trace( | ||
@@ -1394,3 +1291,11 @@ `resolveFilter ${expr.type === 'OpCall' ? `${expr.type}/${expr.op}` : expr.type} %O`, | ||
): TypeNode { | ||
return mapConcrete(node, scope, (base) => (base.type === 'array' ? mapper(base) : {type: 'null'})) | ||
return mapNode(node, scope, (base) => { | ||
if (base.type === 'unknown') { | ||
return base | ||
} | ||
if (base.type === 'array') { | ||
return mapper(base) | ||
} | ||
return {type: 'null'} | ||
}) | ||
} | ||
@@ -1403,5 +1308,11 @@ | ||
): TypeNode { | ||
return mapConcrete(node, scope, (base) => | ||
base.type === 'object' ? mapper(base) : {type: 'null'}, | ||
) | ||
return mapNode(node, scope, (base) => { | ||
if (base.type === 'unknown') { | ||
return base | ||
} | ||
if (base.type === 'object') { | ||
return mapper(base) | ||
} | ||
return {type: 'null'} | ||
}) | ||
} |
@@ -0,1 +1,2 @@ | ||
import type {ExprNode} from '../nodeTypes' | ||
import {optimizeUnions} from './optimizations' | ||
@@ -14,2 +15,3 @@ import type {Scope} from './scope' | ||
UnionTypeNode, | ||
UnknownTypeNode, | ||
} from './types' | ||
@@ -98,16 +100,11 @@ | ||
/** | ||
* mapConcrete extracts a _concrete type_ from a type node, applies the mapping | ||
* mapNode extracts either a _concrete type_ OR an _unknown type_ from a type node, applies the mapping | ||
* function to it and returns. Most notably, this will work through unions | ||
* (applying the mapping function for each variant) and inline (resolving the | ||
* reference). | ||
* | ||
* An `unknown` input type causes it to return `unknown` as well. | ||
* | ||
* After encountering unions the resulting types gets passed into `mergeUnions`. | ||
* By default this will just union them together again. | ||
*/ | ||
export function mapConcrete( | ||
**/ | ||
export function mapNode<T extends TypeNode = TypeNode>( | ||
node: TypeNode, | ||
scope: Scope, | ||
mapper: (node: ConcreteTypeNode) => TypeNode, | ||
mapper: (node: ConcreteTypeNode | UnknownTypeNode) => T, | ||
mergeUnions: (nodes: TypeNode[]) => TypeNode = (nodes) => | ||
@@ -123,10 +120,9 @@ optimizeUnions({type: 'union', of: nodes}), | ||
case 'number': | ||
case 'unknown': | ||
return mapper(node) | ||
case 'unknown': | ||
return node | ||
case 'union': | ||
return mergeUnions(node.of.map((inner) => mapConcrete(inner, scope, mapper), mergeUnions)) | ||
return mergeUnions(node.of.map((inner) => mapNode(inner, scope, mapper), mergeUnions)) | ||
case 'inline': { | ||
const resolvedInline = resolveInline(node, scope) | ||
return mapConcrete(resolvedInline, scope, mapper, mergeUnions) | ||
return mapNode(resolvedInline, scope, mapper, mergeUnions) | ||
} | ||
@@ -138,1 +134,9 @@ default: | ||
} | ||
export function isFuncCall(node: ExprNode, name: string): boolean { | ||
if (node.type === 'Group') { | ||
return isFuncCall(node.base, name) | ||
} | ||
return node.type === 'FuncCall' && `${node.namespace}::${node.name}` === name | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1024987
52
13780