-
Notifications
You must be signed in to change notification settings - Fork 24
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
fix(typeEvaluator): fix in-operator union handling #256
Conversation
8e5ce9d
to
080d612
Compare
src/typeEvaluator/typeHelpers.ts
Outdated
* This method should _only_ be used if you need to handle unknown types, ie when resolving two sides of an and node, and we don't want to abort if one side is unknown. | ||
* In most cases, you should use `mapConcrete` instead. | ||
**/ | ||
export function mapUnion( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit annoying to need this, but the reason is that when resolving "and" conditions like _id == $id && _type == "foo"
we don't want to short circuit the mapper when walking _id == $id
(resolves to unknown
because parameter is unknown
).
Alternatively we can use mapUnion in handleOpCallNode
instead, and there do a unknown check so it at least returns a boolean.
function handleOpCallNode(node: OpCallNode, scope: Scope): TypeNode {
$trace('opcall.node %O', node)
const lhs = walk({node: node.left, scope})
const rhs = walk({node: node.right, scope})
return mapUnion(lhs, scope, (left) =>
// eslint-disable-next-line complexity, max-statements
mapUnion(rhs, scope, (right) => {
$trace('opcall.node.concrete "%s" %O', node.op, {left, right})
if (left.type === 'unknown' || right.type === 'unknown') {
return {type: 'boolean'}
}
Then we would resolve the correct type for a projection like:
{
"foo": $myParameter == 1
}
Which currently resolves to unknown
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_id == $id
(resolves to unknown because parameter is unknown)
Just a quick comment (before reading the rest of the problem): ==
(and !=
) are one of the few operators which actually always return true
/false
(null
is not possible). So regardless of what the RHS is then this will be boolean
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And similarly: Comparisons operators are always boolean | null
. We should always be able to fallback to that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change this into function handleOpCallNode
handling unknowns with mapUnion, and ==
/!=
will now always return a boolean
, while the other operators short circuits to unknown
if an unknown
is given.
test/typeEvaluate.test.ts
Outdated
@@ -838,7 +889,7 @@ t.test('values in projection', (t) => { | |||
type: 'objectAttribute', | |||
value: { | |||
type: 'boolean', | |||
value: undefined, | |||
value: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
neither foo
nor bar
exists on the document, so this will always be false
@@ -1313,7 +1364,7 @@ t.test('with select, not guaranteed & with fallback', (t) => { | |||
const query = `*[_type == "author" || _type == "post"] { | |||
_type, | |||
"something": select( | |||
_id > 5 => _id, | |||
_id == "5" => _id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_id is a string, so fixed the comparison
"unknownParent": ^._id, | ||
"unknownParent2": ^.^.^.^.^.^.^.^._id, | ||
"andWithAttriute": !false && !someAttriute, | ||
"andWithAttribute": !false && !someAttribute, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a typo, but added the actual attribute so that the test makes sense
080d612
to
aa37dab
Compare
e86ddb0
to
b5bb564
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having evaluate type tests for And
and Or
will probably uncover potential bugs around the handling.
t.test(`And`, async (t) => {
subtestBinary({
t,
variants1: trivialVariant,
variants2: trivialVariant,
build: (left, right) => ({type: 'And', left, right})
})
})
t.test(`Or`, async (t) => {
subtestBinary({
t,
variants1: trivialVariant,
variants2: trivialVariant,
build: (left, right) => ({type: 'Or', left, right})
})
})
src/typeEvaluator/typeHelpers.ts
Outdated
* This method should _only_ be used if you need to handle unknown types, ie when resolving two sides of an and node, and we don't want to abort if one side is unknown. | ||
* In most cases, you should use `mapConcrete` instead. | ||
**/ | ||
export function mapUnion( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_id == $id
(resolves to unknown because parameter is unknown)
Just a quick comment (before reading the rest of the problem): ==
(and !=
) are one of the few operators which actually always return true
/false
(null
is not possible). So regardless of what the RHS is then this will be boolean
.
src/typeEvaluator/typeHelpers.ts
Outdated
* This method should _only_ be used if you need to handle unknown types, ie when resolving two sides of an and node, and we don't want to abort if one side is unknown. | ||
* In most cases, you should use `mapConcrete` instead. | ||
**/ | ||
export function mapUnion( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And similarly: Comparisons operators are always boolean | null
. We should always be able to fallback to that.
50c1024
to
17de1e4
Compare
// 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'} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit hacky, but just needed to short circuit the operator if used with path.
Another approach, that might come in a follow-up PR is to put the scope into filterMode
, and we can then use that to determine what typenode to return in the function. This could also then be used to rewrite the scope when using literal comparisons or the defined
method
17de1e4
to
856a493
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One proper request: The handling of null
+ true
seems broken in And/Or. I think this would have been covered by adding a typeEvaluateCompare
test where we add union({type: 'null'}, …)
. Currently we only union by unknown
. It's a bit tricky to add this test without exploding all of the variants though. We might need more fine-grained per-op variations…
One suggestion which can be done separately from this PR: Don't distinguish between mapConcrete
and mapUnion
, but always use one (I propose keeping the name mapConcrete
) which also requires you to handle unknown
.
src/typeEvaluator/typeEvaluate.ts
Outdated
const leftValue = booleanValue(lhs) | ||
const rightValue = booleanValue(rhs) | ||
if (leftValue.canBeTrue || rightValue.canBeTrue) { | ||
if (leftValue.canBeFalse || rightValue.canBeFalse) { | ||
if (leftValue.canBeNull || rightValue.canBeNull) { | ||
return nullUnion({type: 'boolean'}) | ||
} | ||
|
||
return {type: 'boolean'} | ||
} | ||
|
||
return {type: 'boolean', value: true} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this wrong if both values are [canBeTrue, canBeNull]
and [canBeTrue, canBeNull]
? 🤔
In which case the actual type should be nullIUnion({type: 'boolean'})
, but here it would be detected as {type: 'boolean', value: true}
?
I think potentially this can be easier expressed by defining the logic on BooleanInterpretation:
function booleanOr(left: BooleanInterpretation, right: BooleanInterpretation): BooleanInterpretation {
// One side is true => return `true`
if (left.canBeTrue && !left.canBeFalse && !left.canBeNull) return left
if (right.canBeTrue && !right.canBeFalse && !right.canBeNull) return right
// One side is false => return the other side
if (!left.canBeTrue && left.canBeFalse && !left.canBeNull) return right
if (!right.canBeTrue && right.canBeFalse && !right.canBeNull) return left
// Guaranteed to be boolean at least:
if (!left.canBeNull && !right.canBeNull) return {canBeTrue: true, canBeFalse: true, canBeNull: false}
// Fallback to the most permissive:
return {canBeTrue: true, canBeFalse: true, canBeNull: true}
}
I think this covers the main cases.
And then and
becomes quite symmetric:
function booleanAnd(left: BooleanInterpretation, right: BooleanInterpretation): BooleanInterpretation {
// One side is true => return that side
if (left.canBeTrue && !left.canBeFalse && !left.canBeNull) return right
if (right.canBeTrue && !right.canBeFalse && !right.canBeNull) return left
// One side is false => return `false`
if (!left.canBeTrue && left.canBeFalse && !left.canBeNull) return left
if (!right.canBeTrue && right.canBeFalse && !right.canBeNull) return right
// Guaranteed to be boolean at least:
if (!left.canBeNull && !right.canBeNull) return {canBeTrue: true, canBeFalse: true, canBeNull: false}
// Fallback to the most permissive:
return {canBeTrue: true, canBeFalse: true, canBeNull: true}
}
bc3c41e
to
3da81f0
Compare
3da81f0
to
a4bb1e3
Compare
…ions feat(typeEvaluator): separate booleans logic into functions
a4bb1e3
to
3eb2f48
Compare
Fixed in #260 |
Two changes is involved in this pr:
* This pr builds on top of #253 which fixes in-operator on union types
* Removes resolveCondition so that we can reuse types that are walked when we are resolving filters.