False negative for valid dereference of single element collection

Issue #2426 resolved
Scott Wells repo owner created an issue

This was raised here:

https://salesforce.stackexchange.com/questions/410161/assignment-of-type-mapid-sobject-values-property-to-object

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)

  1. Scott Wells reporter

    I’ve just implemented and committed a fix for this that ensures that Map.values() evaluates to both List<ValueType> and ValueType 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.

  2. Scott Wells 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.

  3. Eric Kintzer

    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.

  4. Log in to comment