-
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(typeEvlauator): handle rest on object splat operations #232
Conversation
8ef8b24
to
15ec7ac
Compare
Something looks a bit sketchy. Here we invoke the mapper multiple times: groq-js/src/typeEvaluator/typeEvaluate.ts Lines 124 to 128 in 15ec7ac
But here we assume that it's only being called once per groq-js/src/typeEvaluator/typeEvaluate.ts Lines 165 to 167 in 15ec7ac
I'm not 100% sure what the right approach here is though. Maybe more usage of |
yess, this should probably be a concrete type with mapConcrete, will fix. Didn't think much about fixing other bugs than the one referenced 😅 |
@judofyr did another pass on comparing with mapped concrete type node |
1558ef1
to
8992280
Compare
src/typeEvaluator/typeEvaluate.ts
Outdated
const attributes: Record<string, ObjectAttribute> = {} | ||
|
||
const concreteNode = mapConcrete(node, scope, (node) => node) | ||
if (concreteNode.type === 'object') { |
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.
Hmm. Doesn't this mean that if this returns false
we end up returning {type: 'object', attributes: {}}
below? What about we instead wrap all of this in mapConcrete
?
return mapConcrete(node, scope, (node) => {
if (node.type !== 'object') return {type: 'unknown'}
const attributes: Record<string, ObjectAttribute> = {}
// same same…
})
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.
I did another pass, moved the handling of the object splats out in its own methods
fa78ba0
to
05e70bc
Compare
src/typeEvaluator/typeEvaluate.ts
Outdated
if (result.type !== 'object') { | ||
return result | ||
} |
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.
I don't think this makes sense: If the splatted node is not an object then we return that as the result of the whole object?
In the following query:
*[_type == "author"][0] {
_type == "author" => select(age >= 18 => {lastname}, {firstname}),
_id,
_type
}
Then we now end up generating:
{
"type": "union",
"of": Array [
Object {
"type": "object",
"attributes": Object {
"firstname": Object {
"type": "objectAttribute",
"value": Object {
"type": "string",
},
},
},
},
Object {
"type": "object",
"attributes": Object {
"lastname": Object {
"type": "objectAttribute",
"value": Object {
"type": "string",
},
},
},
},
Object {
"type": "null",
},
],
}
Because the "conditional splat" turns into a union
and thus we hit this case.
src/typeEvaluator/typeEvaluate.ts
Outdated
if (mapped.type != 'object') { | ||
return mapped | ||
} | ||
for (const name in mapped.attributes) { | ||
if (!mapped.attributes.hasOwnProperty(name)) { | ||
continue | ||
} | ||
attributes[name] = mapped.attributes[name] | ||
} | ||
return {type: 'object', attributes} |
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.
This code has just basically takes a type: "object"
and then makes a copy of it without rest
or dereferencesTo
. That doesn't look right to me.
src/typeEvaluator/typeEvaluate.ts
Outdated
if (concreteRest.type !== 'object') { | ||
return {type: 'null'} |
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.
This means that if concreteRest.type
is "union" we end up returning null?
It's a bit unclear to me what the intended behavior of handling objects with unions and inlines are at the moment. If there's union in any of
Maybe we can add some more fine-grained tests using |
How about creating some tests like this (maybe turn it into a table-based one?): const nodeWithType = (type: TypeNode) => {
const expr: ExprNode = {type: 'Value', value: null}
overrideTypeForNode(expr, type)
return expr
}
t.test('objects', (t) => {
// test 1
typeEvaluate(
{
type: 'Object',
attributes: [
{type: 'ObjectAttributeValue', name: 'foo', value: nodeWithType({type: 'string'})},
],
},
[],
)
// test 2
typeEvaluate(
{
type: 'Object',
attributes: [
{type: 'ObjectAttributeValue', name: 'foo', value: nodeWithType({type: 'string'})},
{type: 'ObjectAttributeValue', name: 'foo', value: nodeWithType({type: 'number'})},
],
},
[],
)
// test 3
typeEvaluate(
{
type: 'Object',
attributes: [
{type: 'ObjectAttributeValue', name: 'foo', value: nodeWithType({type: 'string'})},
{type: 'ObjectSplat', value: nodeWithType({type: 'unknown'})},
],
},
[],
)
}) |
f566358
to
7767d39
Compare
I've addressed your feedback(100% agree), we should now be handling unions by always using mapConcrete. I also moved resolveInline into a separate function so we can be explicit about resolving inline nodes. |
test/typeEvaluate.test.ts
Outdated
expects: unionOf( | ||
{ | ||
type: 'object', | ||
attributes: { | ||
foo: {type: 'objectAttribute', value: {type: 'boolean'}}, | ||
}, | ||
}, | ||
{ | ||
type: '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.
Is there any value in returning {foo: boolean} | unknown
instead of just unknown
here?
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.
We could short circuit so that if any of the returned elements are unknown we return unknown. Guess this could happen if you have a schema where a field is either an object or and array of something, not sure that's very realistic, though it could be during a migration for example.
If you have multiple attributes in the object, you could simplify the type check by just asserting one value and typescript would assume it's not the unknown object 🤔
Personally no strong opinions
test/typeEvaluate.test.ts
Outdated
}, | ||
|
||
{ | ||
name: 'Test splatting over multiple conditionals leads to a matrix of all possible combinations', |
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.
So, here we have:
- Attribute with
noah
- Conditional splat with
bear | fox | deer
- Conditional splat with
deer | elk | moose
- Conditional splat with
ox | pig | chicken
Wouldn't one possible choice here be an object with {noah, beer, deer, ox}
? If all the conditionals match? I can't find that in the list.
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.
Also, it's way more common to have conditional splats with a plain object (e.g. _type == "foo" => {abc, def}
) and not a union so it would be nice to have some tests for that as well.
ed0356c
to
49a4266
Compare
cb8f61d
to
3c2908b
Compare
Pushed my latest changes, there's optimizations to be done in the object handler itself(probably way too many loops 😅), but if it's correct I can give optimizations another passs |
3c2908b
to
01e0726
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.
I've mainly focused on the tests and they all look sensible to me.
(I haven't looked much at the implementation. Let me know if there's anything particular there you'd like me to review. Otherwise I'm pretty happy doing a "test-first" approach for this code anyway.)
9007ba9
to
0fbbcbe
Compare
0fbbcbe
to
1c99c96
Compare
When splatting an object we didn't map over the rest property, if it's an object we should append the attributes. If
rest
is unknown we should treat the entire object as unknown.Fixes sanity-io/sanity#6555