False negative for valid dereference of single element collection
This was raised here:
and I’m trying to generalize it a bit as follows:
public with sharing class SingleElementReferenceTest {
public static void testSingleSobjectElementReference() {
Map<Id, Account> accountsById = new Map<Id, Account>([SELECT Id, AccountNumber FROM Account LIMIT 1]);
// This should work but can lead to errors if there's not exactly one account in the map
System.debug(accountsById.values().AccountNumber);
// This is also fine, so the key type doesn't seem to matter
Map<Datetime, Account> accountsByName = new Map<Datetime, Account>();
Account account = [SELECT Name, AccountNumber, LastModifiedDate FROM Account LIMIT 1];
accountsByName.put(account.LastModifiedDate, account);
System.debug(accountsByName.values().AccountNumber);
}
public class InnerValueObject {
public String myProperty = UserInfo.getUserName();
}
public static void testSingleElementReference() {
Map<Id, InnerValueObject> innerValueObjectMap = new Map<Id, InnerValueObject>();
innerValueObjectMap.put(UserInfo.getOrganizationId(), new InnerValueObject());
// This compiles but GACKs, presumably because the map value type is not an SObject type
System.debug(innerValueObjectMap.values().myProperty);
// This does not compile
List<InnerValueObject> innerValueObjects = innerValueObjectMap.values();
System.debug(innerValueObjects.myProperty);
// Nor does this
List<InnerValueObject> innerValueObjectList = new List<InnerValueObject>();
System.debug(innerValueObjectList.myProperty);
// Nor does this
InnerValueObject[] innerValueObjectArray = new InnerValueObject[] { new InnerValueObject() };
System.debug(innerValueObjectArray.myProperty);
// Nor does this
Set<InnerValueObject> innerValueObjectSet = new Set<InnerValueObject>();
System.debug(innerValueObjectSet.myProperty);
}
}
As is likely evident from the example above, it’s difficult to tell exactly when this should be considered valid and when it shouldn’t be, though as stated in the linked SE Q&A above, it seems like Map.values()
specifically should be handled in a similar fashion to a query result and allowed to evaluate to a list or single instance of the contained data (not just SObject
).
This is one of those where I’m torn between supporting the idiom for “correctness” vs. flagging it to force users to think about the potential pitfalls of using it. I think that perhaps the right solution is to update IC2’s expression type evaluator to allow it, but at the same time to add a code inspection to flag it as a dangerous idiom as there’s no way of knowing at compile-time whether the returned collection has exactly one element.
Once again, Apex has some weird behaviors sometimes…
Comments (7)
-
reporter -
reporter A few additional findings on this…it seems that if the map value type is not an SObject type, e.g., the
myProperty
example from above, this idiom compiles but results in a GACK at runtime:An unexpected error occurred. Please include this ErrorId if you contact support: 2102716977-191403 (-980699288)
Right now IC2’s expression type evaluator accepts this usage just as the compiler does, but I think I’ll probably make the same code inspection that’s going to help make such usages more defensive also report the potential for a GACK in this situation.
-
FWIW, I sent the SFSE item via Twitter (X) to Daniel Ballinger, Apex PM but I’m sure he’s busy with Dreamforce right now - having the compiler allow something that resolves at runtime for apexTypes to a GACK doesn't seem good to me.
-
reporter - edited description
-
reporter - edited description
-
reporter - edited description
-
reporter - changed status to resolved
Delivered in 2.2.8.8.
- Log in to comment
I’ve just implemented and committed a fix for this that ensures that
Map.values()
evaluates to bothList<ValueType>
andValueType
based on the usage just like a SOQL query:As stated above, I’ll likely implement a code inspection to flag this as a warning with quick fixes to expand the single element dereferencing into something more defensive.