Apex code generation

Issue #25 resolved
Scott Wells repo owner created an issue

Add code generation options for:

  • Constructors
  • Getters/setters - DONE
  • Implement/override methods from base class/implemented interfaces - DONE
  • equals()/hashCode() - DONE
  • Unit test classes/methods/setup

Comments (25)

  1. Piotr Kożuchowski

    Bump - I really miss generator for constructors and I was on verge of submitting enhancement number of times already.

    Thanks!

  2. Scott Wells reporter

    This will be coming relatively soon. I need to wrap up some LWC enhancements and then will be moving back to more refactorings, code inspections/intentions, and code generation.

  3. Scott Wells reporter

    Piotr (and anyone else), is there a specific subset of this that you were looking for? The main one I'd heard about from people was implement/override so I knocked that one out. I'm happy to revisit high priority subsets of this.

  4. Piotr Kożuchowski

    I think the constructor generation is the main one on my mind - to be able to select class fields for a constructor.

  5. Scott Wells reporter

    Gotcha. Anyone else have a favorite code generation aspect that's not yet implemented?

  6. Justin Julicher

    In addition to Piotr’s request when a constructor already exists add a field to the constructor(s) and assign it?

    In the example below you would have an option in the context menu of the field to “add to Constructor(s)”

    Ideally it would give you the method dialog with the ability to change the order of the current parameters and then open the refactor dialog to inspect the references? Not sure how this should be handled but I seem to remember this functionality existing in IntelliJ Idea for Java?

    e.g.

    public class TestClass {
    private String myField;
    
      public TestClass(){
        System.debug('Some line in class');
      }
    }
    

    would go to

    public class TestClass {
    
      private String myField;
    
      public TestClass(String myField){
        System.debug('Some line in class');
        this.myField = myField;
      }
    }
    

  7. Matt Simonis Account Deactivated

    Some additional improvements currently supported by Java that would be nice:

    • Getter
    • Setter
    • Getter and Setter
    • equals() and hashCode()

    Thanks!

  8. Scott Wells reporter

    Okay, I’m finally putting some significant effort into new Apex code generation capabilities. I’m starting with equals() and hashCode() generation, but rest assured that I do plan to implement code generation for constructors and field getters/setters as well…and maybe even some code intentions to convert fields to properties and vice-versa.

    I thought I’d solicit some feedback from folks here about the generated implementations of equals() and hashCode(). In Java, there are quite a few libraries to assist with those--Apache Commons, Guava, and even standard in more recent versions of the JDK--but in Apex (ignoring third-party libs), the only options are System.equals() and System.hashCode(). Those have significant limitations, though, as the former requires the first parameter to be non-null and the latter requires its one parameter to be non-null. Also, System.equals() has the same behavior as the == operator in Apex, so equivalence checks don’t necessarily behave in the expected manner, specifically case-insensitive comparison of String values.

    Right now I’m generating code like the following:

    public with sharing class CodeGenerationTest {
        private String firstName;
        private String lastName;
        private Date dob;
        private Integer age;
        private Boolean licensed;
        private Account account;
    
        public Boolean equals(Object o)     {
            if (this == o) return true;
            if (!(o instanceof CodeGenerationTest)) return false;
    
            CodeGenerationTest that = (CodeGenerationTest) o;
    
            return firstName.equals(that.firstName) &&
                lastName.equals(that.lastName) &&
                System.equals(dob, that.dob) &&
                (((age != null) && System.equals(age, that.age)) || ((that.age != null) && System.equals(that.age, age)) || (age == that.age)) &&
                System.equals(licensed, that.licensed) &&
                (((account != null) && System.equals(account, that.account)) || ((that.account != null) && System.equals(that.account, account)) || (account == that.account));
        }
    
        public Integer hashCode() {
            Integer result = System.hashCode(firstName);
            result = (31 * result) + System.hashCode(lastName);
            result = (31 * result) + System.hashCode(dob);
            result = (31 * result) + (age != null ? System.hashCode(age) : 0);
            result = (31 * result) + System.hashCode(licensed);
            result = (31 * result) + (account != null ? System.hashCode(account) : 0);
            return result;
        }
    }
    

    In the example above, I told the generator that firstName, lastName, dob, and licensed are always non-null, but other fields (this also works for properties) can be null. This all leads to a few special behaviors in the generated code:

    1. When a field is of type String, it is checked for equivalence using String.equals(); otherwise System.equals() is used. It’s possible that there should be other type-specific exceptions, e.g., for Id.
    2. When a field value can be assumed to be non-null, a direct comparison or call to System.hashCode() can be made with no need to check for a null value first.
    3. When a field value cannot be assumed to be non-null, a series of checks is performed to determine equivalence:

      1. Check the first operand as non-null and equivalent to the second operand using the type-appropriate check as described above.
      2. If not, check the second operand as non-null and equivalent to first operand using the type-appropriate check as described above.
      3. If not, check the operands as equivalent using the == operator. This would really only be used to check for null == null as the previous checks should have sussed out all non-null scenarios.

    It’s also worth talking about this line for a moment:

    if (!(o instanceof CodeGenerationTest)) return false;
    

    In a Java implementation, this would be:

    if ((o == null) || (getClass() != o.getClass())) return false;
    

    but the latter condition isn’t possible in Apex. As a result, while the check I’ve added would allow subclass comparisons, a type assignability check needs to happen in some form given the very next line casts to the expected type.

    As for the hashCode() implementation, it’s pretty much stock-standard with null checks as appropriate.

    So…is anyone aware of a better way to implement these generically in Apex assuming only the system/standard Apex types? I’m inclined to move forward with these implementations and see how it goes, but certainly if there are opportunities to improve the generated code for either/both of these methods before putting it into the wild, all the better. Thanks in advance for any thoughts you might have on this!

  9. Scott Wells reporter

    I’ve also implemented getter and/or setter generation for the next build. I’ll be doing this in phases with constructor generation likely being the main focus of the second phase. I want to make sure that’s implemented properly as it’s something I use non-stop in Java myself including all of the nuances around field initialization, etc.

  10. Scott Wells reporter

    Enhancements from #2418 were delivered in 2.2.8.2.

    With these three phases of work complete, I'm going to declare victory on this one even though I haven't yet implemented anything related to Apex unit test generation. Once I have my head wrapped around what Einstein GPT does/doesn't do in that regard (and the level of availability for that feature), I'll revisit test generation in the context of another enhancement request.

  11. Log in to comment