Skip to content
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(compiler): Compiler errors for missing keys in iterator #138

Merged
merged 16 commits into from Mar 16, 2018

Conversation

davidturissini
Copy link
Contributor

@davidturissini davidturissini commented Mar 6, 2018

Details

Not including a key in iterators(for:each and iterator:foo) will now produce a compile time error. Furthermore, using duplicate keys inside of an iteration will now warn the user as this is most likely a mistake and will result in incorrect markup.

Does this PR introduce a breaking change?

  • Yes
  • No

If yes, please describe the impact and migration path for existing applications:
Please check if your PR fulfills the following requirements:

We are currently auto-generating keys in iterators via a weak map in api.k. This is proving to be problematic so we have decided to change direction and require devs to supply a key value themselves. This will break downstream components.

Fixes #136

@@ -255,6 +254,11 @@ export function i(iterable: Iterable<any>, factory: (value: any, index: number,
let next = iterator.next();
let j = 0;
let { value, done: last } = next;
if (process.env.NODE_ENV !== 'production') {
// var is intentional here, function level scoping is required.
var keyMap = {};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't think of a better way to hoist a map of used keys that will get populated in the while loop. Suggestions happily taken :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this a little bit more, I think this will still get minified if I use let outside of the prod check

@@ -271,9 +275,14 @@ export function i(iterable: Iterable<any>, factory: (value: any, index: number,
if (process.env.NODE_ENV !== 'production') {
const vnodes = isArray(vnode) ? vnode : [vnode];
vnodes.forEach((childVnode) => {
if (!isNull(childVnode) && isObject(childVnode) && !isUndefined(childVnode.sel) && childVnode.sel.indexOf('-') > 0 && isUndefined(childVnode.key)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want the childVnode.sel.indexOf('-') check here. This is valid for all elements, not just custom

objToKeyMap.set(unwrapped, objKey);
}
return compilerKey + ':' + objKey;
throw new Error(`Invalid key value ${obj}. Key must be a string or number.`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opted for an Error instead of assert because I believe we will want this in production.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you will have to do toString() around this, otherwise it might throw that it can't be stringify.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error is only incomplete, we should at least tell them what VM is this, but in prod we don't have toString on VM.

my recommendation is to keep it as an assert until the next refactor of the errors.

@@ -122,23 +122,3 @@ export function getForEachParent(element: IRElement): IRElement | null {

return null;
}

export function keyExpression(element: IRElement) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need to climb the iterator tree to magically bind the iterator value to our key. Instead, if the user has not defined their own key, we throw.

@@ -110,7 +111,7 @@ export default function parse(source: string, state: State): {
applyHandlers(element);
applyComponent(element);
applySlot(element);
applyKey(element);
applyKey(element, elementNode.__location);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmdartus __location seems like a bad idea to use here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The element has access to the original node from the __original property.

No need to pass the location there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like __original is an instance of HTMLElement, not parse5.AST.Default.Element

@@ -373,10 +374,9 @@ export default function parse(source: string, state: State): {
}

element.forKey = keyAttribute.value;
} else if ((getIteratorParent(element) || getForEachParent(element)) && element.tag !== 'template') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can possibly be abstracted out to isIteratorElement that returns true is element will be iterated over.

@@ -255,6 +254,11 @@ export function i(iterable: Iterable<any>, factory: (value: any, index: number,
let next = iterator.next();
let j = 0;
let { value, done: last } = next;
let keyMap;
if (process.env.NODE_ENV !== 'production') {
keyMap = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create(null) to avoid surprises.

// TODO - it'd be nice to log the owner component rather than the iteration children
assert.logWarning(`Missing "key" attribute in iteration with child "<${childVnode.sel}>", index ${i}. Instead set a unique "key" attribute value on all iteration children so internal state can be preserved during rehydration.`);
if (!isNull(childVnode) && isObject(childVnode) && !isUndefined(childVnode.sel)) {
if (isUndefined(childVnode.key)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about key being null?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably better to just test for typeof to be number or string

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 02a1320 | Target commit: 9d1c60c

benchmark base(02a1320) target(9d1c60c) trend
table-append-1k.benchmark:benchmark-table/append/1k 274.44 (± 5.51 ms) 263.92 (± 4.21 ms) 👍
table-clear-1k.benchmark:benchmark-table/clear/1k 14.28 (± 0.57 ms) 12.20 (± 0.32 ms) 👍
table-create-10k.benchmark:benchmark-table/create/10k 1589.07 (± 31.51 ms) 1521.79 (± 27.64 ms) 👍
table-create-1k.benchmark:benchmark-table/create/1k 167.12 (± 2.00 ms) 156.81 (± 1.99 ms) 👍
table-update-10th-1k.benchmark:benchmark-table/update-10th/1k 150.52 (± 2.99 ms) 130.47 (± 1.45 ms) 👍
tablecmp-append-1k.benchmark:benchmark-table-component/append/1k 351.38 (± 2.33 ms) 356.76 (± 8.25 ms) 👎
tablecmp-clear-1k.benchmark:benchmark-table/clear/1k 34.76 (± 1.52 ms) 35.13 (± 1.66 ms) 👌
tablecmp-create-10k.benchmark:benchmark-table-component/create/10k 2677.99 (± 20.79 ms) 2775.77 (± 61.84 ms) 👎
tablecmp-create-1k.benchmark:benchmark-table-component/create/1k 310.25 (± 12.09 ms) 297.95 (± 6.76 ms) 👍
tablecmp-update-10th-1k.benchmark:benchmark-table-component/update-10th/1k 146.16 (± 2.86 ms) 121.22 (± 1.48 ms) 👍

@pmdartus
Copy link
Member

pmdartus commented Mar 7, 2018

I have some usability concern with this change, especially with external customers. Even if React and Vue offer the capability to add keys but doesn't require it.

I would expect that if we throw at compile time, we will run into the same issue than the React community is running into where people use the array index has a key. If we really want to proceed with this change, we should also ensure that the key is not bound to the for:index identifier.

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 02a1320 | Target commit: 3a87e45

benchmark base(02a1320) target(3a87e45) trend
table-append-1k.benchmark:benchmark-table/append/1k 274.44 (± 5.51 ms) 255.63 (± 3.38 ms) 👍
table-clear-1k.benchmark:benchmark-table/clear/1k 14.28 (± 0.57 ms) 12.30 (± 0.45 ms) 👍
table-create-10k.benchmark:benchmark-table/create/10k 1589.07 (± 31.51 ms) 1506.64 (± 15.67 ms) 👍
table-create-1k.benchmark:benchmark-table/create/1k 167.12 (± 2.00 ms) 157.43 (± 2.51 ms) 👍
table-update-10th-1k.benchmark:benchmark-table/update-10th/1k 150.52 (± 2.99 ms) 127.99 (± 1.92 ms) 👍
tablecmp-append-1k.benchmark:benchmark-table-component/append/1k 351.38 (± 2.33 ms) 349.50 (± 7.57 ms) 👌
tablecmp-clear-1k.benchmark:benchmark-table/clear/1k 34.76 (± 1.52 ms) 33.98 (± 1.48 ms) 👌
tablecmp-create-10k.benchmark:benchmark-table-component/create/10k 2677.99 (± 20.79 ms) 2678.82 (± 16.63 ms) 👌
tablecmp-create-1k.benchmark:benchmark-table-component/create/1k 310.25 (± 12.09 ms) 293.09 (± 6.59 ms) 👍
tablecmp-update-10th-1k.benchmark:benchmark-table-component/update-10th/1k 146.16 (± 2.86 ms) 118.05 (± 1.97 ms) 👍

@caridy
Copy link
Contributor

caridy commented Mar 7, 2018

I have some usability concern with this change, especially with external customers. Even if React and Vue offer the capability to add keys but doesn't require it.

We should start with the more restrictive, and eventually relax if possible, as we always do!

I would expect that if we throw at compile time, we will run into the same issue than the React community is running into where people use the array index has a key. If we really want to proceed with this change, we should also ensure that the key is not bound to the for:index identifier.

I think this is a good suggestion, and in the compiler, we can know for sure if the identifier is the one defined in the iteration as the index, and throw for now.

@caridy
Copy link
Contributor

caridy commented Mar 15, 2018

guys, this should be part of the next release, otherwise people will face issues that they can't explain when no keys are provided.

@davidturissini
Copy link
Contributor Author

davidturissini commented Mar 15, 2018

@caridy do we still want to implement throwing when the user uses index?

@caridy
Copy link
Contributor

caridy commented Mar 15, 2018

@davidturissini yes, I think we should do that to avoid any confusion.

keyMap[key] = 1;
} else {
// TODO - it'd be nice to log the owner component rather than the iteration children
assert.logWarning(`Missing "key" attribute in iteration with child "<${childVnode.sel}>", index ${i}. Instead set a unique "key" attribute value on all iteration children so internal state can be preserved during rehydration.`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead set a unique "key" - 'instead' word is not needed here since there was no alternative. We can just start with 'Set a unique key ...'

@@ -3,7 +3,7 @@
"length": 11,
"level": "error",
"message": "Key attribute value should be an expression",
"start": 83
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is a big gap 83 vs 19

if (keyAttribute.type !== IRAttributeType.Expression) {
return warnAt(`Key attribute value should be an expression`, keyAttribute.location);
}

if (isForOfChild(element)) {
if (keyAttribute.value.property.name === 'index') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmdartus There is a type error here and it's difficult to trace exactly what keyAttribute should be. Typescript doesn't like me looking at property key on value. Any tips?

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 7da31a7 | Target commit: cf84fc1

benchmark base(7da31a7) target(cf84fc1) trend
table-append-1k.benchmark:benchmark-table/append/1k 661.43 (± 32.60 ms) 248.32 (± 3.46 ms) 👍
table-clear-1k.benchmark:benchmark-table/clear/1k 17.89 (± 2.44 ms) 11.64 (± 0.33 ms) 👍
table-create-10k.benchmark:benchmark-table/create/10k 1587.87 (± 41.17 ms) 1486.11 (± 18.43 ms) 👍
table-create-1k.benchmark:benchmark-table/create/1k 169.35 (± 11.42 ms) 151.43 (± 1.07 ms) 👍
table-update-10th-1k.benchmark:benchmark-table/update-10th/1k 170.31 (± 17.04 ms) 124.39 (± 1.81 ms) 👍
tablecmp-append-1k.benchmark:benchmark-table-component/append/1k 360.86 (± 15.11 ms) 343.63 (± 6.69 ms) 👍
tablecmp-clear-1k.benchmark:benchmark-table/clear/1k 34.45 (± 1.15 ms) 32.66 (± 1.23 ms) 👍
tablecmp-create-10k.benchmark:benchmark-table-component/create/10k 2635.79 (± 44.14 ms) 2617.89 (± 18.33 ms) 👌
tablecmp-create-1k.benchmark:benchmark-table-component/create/1k 296.73 (± 24.38 ms) 278.52 (± 2.42 ms) 👍
tablecmp-update-10th-1k.benchmark:benchmark-table-component/update-10th/1k 164.30 (± 16.68 ms) 113.94 (± 1.59 ms) 👍

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 7da31a7 | Target commit: 6e215eb

benchmark base(7da31a7) target(6e215eb) trend
table-append-1k.benchmark:benchmark-table/append/1k 661.43 (± 32.60 ms) 267.64 (± 6.70 ms) 👍
table-clear-1k.benchmark:benchmark-table/clear/1k 17.89 (± 2.44 ms) 13.21 (± 0.58 ms) 👍
table-create-10k.benchmark:benchmark-table/create/10k 1587.87 (± 41.17 ms) 1593.33 (± 30.79 ms) 👌
table-create-1k.benchmark:benchmark-table/create/1k 169.35 (± 11.42 ms) 165.37 (± 5.50 ms) 👌
table-update-10th-1k.benchmark:benchmark-table/update-10th/1k 170.31 (± 17.04 ms) 130.15 (± 3.42 ms) 👍
tablecmp-append-1k.benchmark:benchmark-table-component/append/1k 360.86 (± 15.11 ms) 346.28 (± 3.98 ms) 👍
tablecmp-clear-1k.benchmark:benchmark-table/clear/1k 34.45 (± 1.15 ms) 34.84 (± 1.25 ms) 👌
tablecmp-create-10k.benchmark:benchmark-table-component/create/10k 2635.79 (± 44.14 ms) 2754.26 (± 39.70 ms) 👎
tablecmp-create-1k.benchmark:benchmark-table-component/create/1k 296.73 (± 24.38 ms) 288.64 (± 3.77 ms) 👌
tablecmp-update-10th-1k.benchmark:benchmark-table-component/update-10th/1k 164.30 (± 16.68 ms) 119.88 (± 2.64 ms) 👍

@davidturissini davidturissini merged commit de8ee82 into master Mar 16, 2018
@davidturissini davidturissini deleted the dturissini/issue-136/throw-no-key branch March 16, 2018 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants