ClassDefinition.java

/*
** Module   : ClassDefinition.java
** Abstract : defines the 4GL API for a class or interface definition
**
** Copyright (c) 2007-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
** 001 GES 20070911   @35134 First version, with support for resolving member variables,
**                           properties and methods. Honors the public/protected/private
**                           accessor. Manages the inheritance and interface hierarchy.
** 002 GES 20090518   @42405 Import change.
** 003 GES 20110822          Added support for static members. Added flag to differentiate 4GL
**                           classes from .NET classes.
** 004 GES 20140319          Store static and instance methods/vars in different maps since
**                           overloads can occur. Store the least restrictive method/var (from
**                           an access mode perspective) since overloads can occur.
** 005 GES 20160312          Added support for buffers, temp-tables, queries, datasets and
**                           data-sources as members of a class. Protected members can be
**                           accessed from child classes so the lookup processing was enhanced.
**                           Rewrite for common code instead of separate workers.
** 006 HC  20180312          Fixed lookup of STATIC flag of class data members and methods. The 
**                           lookup didn't work correctly during class prescan mode.
**     HC  20180319          Fixed an NPE when a referenced class is not defined (i.e. the class 
**                           file not found). In this case compiler error must be issued.
** 007 GES 20181108          Fixed javadoc.
**     CA  20181113          Fixed lookupHierarchy - in non-static mode, both static and runtime
**                           members must be checked.
**     GES 20181122          Added lookupVariableWrapper().
** 008 GES 20181129          Associate the full filename with any new class or interface def.
**         20181216          Added method_def and method call tempIdx support.
**         20181227          Complete rewrite of method resolution to handle overloading and
**                           parameter wrapping.
** 009 GES 20190118          Changed "access" annotation to "access-mode" for methods. This is
**                           consistent with other resource types.
**     CA  20190219          CONSTRUCTORs are managed same as methods (to be resolved in NEW 
**                           statement).
** 010 CA  20190423          OO calls with POLY arguments are marked as 'dynamic', to emit them
**                           as if a DYNAMIC-INVOKE was used.
**                           Fixed tempidx collisions from pre-scan and normal parsing phases.
**                           Fixed 'var leak' during pre-scan, which prevented correct resolution
**                           of a var reference, if there was a DEF VAR in a method.
** 011 CA  20190520          Solved converted method names for override and overload cases.
**                           The 'provisional' members are required to be resolved once the 
**                           initial pre-scan of a class has finished, by doing a full-parse on it
**                           and any super-classes or interfaces.  This is required because at a 
**                           time a class is referenced, it may not have been by FWD's normal 
**                           processing, and we still need the provisional members solved.
** 012 CA  20190529          Fixed a bug in annotateMethodCall.
** 013 HC  20190714          Allowed handle to be passed to TABLE-HANDLE or DATASET-HANDLE
**                           parameters.
**     CA  20190812          Mark a dynamic function call with 'dynamic-class' if is from a static
**                           context.
** 014 GES 20190620          Added multiple inheritance (only needed for interfaces).  Added enum
**                           support.
**         20190622          Fixed calcFromParentGraph() which was broken in its first version.
**         20190624          Added full .NET dependency tracking including both direct and
**                           indirect cases.
**         20190625          Removed previous .NET dependency tracking.
**         20190710          Fixes to ensure that the dependency reports are complete. Efficiency
**                           improvements.
**         20190903          Added isJava() and isPolySignature() methods.
**     CA  20190927          Added support for direct Java access from 4GL code.
**     RFB 20191008          getExtent has to accommodate the extent that does not include any
**                           square brackets. We are treating this as a dynamic extent.
** 015 RFB 20200214          In addMethod, because the "qualified" annotation is being inserted into
**                           the method's return node (ret) during pre-scan, we want to prevent 
**                           these additional annotation requests, since they aren't in ret.
** 016 CA  20200412          Added incremental conversion support.
**     CA  20200313          Fixed an issue with method call's argument annotation in case of 
**                           chained OO method calls (no need to emit 'new object').
**     CA  20200323          Better fix for the previous fix (OO method call chaining in arg).
**     CA  20200429          Legacy DEFINE ENUM conversion support.
**     CA  20200503          A fix for a variable defined with the same name both in a method and
**                           as a member.
**     GES 20200526          Finished the registration of the virtual enum methods.
**     CA  20200529          Fixed an issue for incremental conversion, when the AST for a skeleton class 
**                           (which was parsed in a previous conversion) does not exist. 
** 017 CA  20200804          Emit bulk setter and getter for an extent property.
**     CA  20210125          Progress.Lang.Class:Invoke and Progress.Reflect.Method:Invoke must be treated as
**                           having a POLY returned type.
**     GES 20210121          Started rework of fuzzy matching to implement missing features.  This first
**                           pass refactors the fuzzyMethodLookup() processing to make a list of all
**                           possible matches at the top of the method (by using the new candidates()
**                           method).  This approach only returns candidates which match the access rights
**                           and number of parameters checks.  It also returns all candidates from the
**                           entire inheritance hierarchy.  This is how the 4GL does it, so that better
**                           matches in the inheritance hierarchy take precedence over local matches.
**                           The parameter mode processing was also switched. INPUT parameter types
**                           widen from caller to callee and OUTPUT parameter types narrow from caller
**                           to callee.
**     CA  20210203          Fixed conversion issues with OUTPUT/INPUT-OUTPUT for extent or scalar parameters,
**                           involved in dynamic or static OO calls, functions or procedure calls.  This 
**                           includes mostly cases for OO properties/variables (static and non-static).
**     CA  20210216          Fixed fuzzy method overload resolution when there are extent arguments - in this
**                           case, the match is 'exact' if the type matches, and both arg and parameter are
**                           extent.
**     IAS 20210219          Made NameConverter instance public
**     CA  20210221          Default to Progress.Lang.Object if an 'object' parameter is missing its qualified
**                           annotation.
**     CA  20210305          For builtin OO method calls, save at the call the typelist for the target method
**                           parameters.
**     CA  20210318          Fixed conversion of extent() function/statement with builtin OO properties.
**     CA  20210609          Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly 
**                           initialized at the method's execution, and not at the caller's arguments.
**     CA  20210616          Fixed incremental conversion for enums - do not mark them as static.
**     GES 20210702          Allow parent hierarchy lookup for static methods.  This is a 4GL quirk that is not
**                           present in Java. See #4978.  Removed dead code and some debugging code. Improved
**                           support for .NET arrays and .NET generic type parameters.
**     OM  20211122          Added hints-based support for 'unloading' schema.
**     CA  20220128          Added a NPE protection for exactMethodLookup(), seen in incremental conversion, 
**                           but with no local duplicate.
**     CA  20220216          Abend if the root Progress.Lang.Object class can't be resolved.
**     ME  20211028          Add support for 'virtual' POLY data type on methods return/parameters.
**     CA  20220307          Legacy OO skeletons are updated to use POLY, removed the 'invoke' checks for 
**                           poly methods, as these will be handled automatically.
**     GES 20220329          Suppress duplicate fuzzy method lookup warnings.
**     TJD 20220504          Java 11 compatibility minor changes
**     CA  20220526          Fixed incremental conversion: buffers can be defined for permanent tables, too.
**     CA  20220428          Annotate THIS-OBJECT and SUPER constructor call parameters.
**     CA  20220513          Track the Java field name for the builtin enums, so they can emit the exact name.
**     CA  20220516          Fixed tracking of extent data members (variables or properties).
**     GES 20210311          Rewrote exact and fuzzy method matching to support all data types and
**                           overloading rules.  Fixed many latent problems.  Reworked how method names
**                           convert to resolve conflicts within the class hierarchy instead of project
**                           wide.
**     CA  20220531          Class PROPERTY members must be loaded only once (when their javaname is computed).
**                           Resolve the skeleton methods' Java name from their FWD implementation.  OVERRIDE
**                           is not mandatory at PROPERTY or METHOD, if overriding an interface property or 
**                           method.  DATASET and TEMP-TABLE are never marked provisional, as they can't be 
**                           defined in a class method.  Fixes for fuzzy matching logic for TABLE-HANDLE, 
**                           TABLE, DATASET, DATASET-HANDLE - they are completely interchangable.
**     CA  20220613          fuzzyMethodLookup must consider the 'TEMP-TABLE <' prefix, too.
**     CA  20220727          OO variable references must get the refid javaname, before checking the javaname 
**                           annotation.
**     CA  20220727          'isInheritedFrom' must consider the directly implemented interfaces, too, not 
**                           just the interfaces from the inherited parent classes.
**     CA  20220727          Improved memory management for parsing; cleanup is done in two phases:
**                           1. after each legacy class file has finished parsing, the SchemaDictionary will
**                              keep only protected temp-tables (all private tables are removed, as these 
**                              can't be reached from sub-classes; all permanent tables are removed, as each
**                              class has its own reference to the permanent tables).  Also, the class def
**                              instance will reduce its own used memory, which is not required when parsing
**                              sub-classes.
**                           2. after parsing of the entire file set is finished, any SchemaDictionary or 
**                              other ASTs referenced by the ClassDefinition are released.
**     CA  20220818          An abstract property can define only a getter or a setter, while the implementation
**                           can define both.  For this reason, only the overridden accessor must have the
**                           @Override annotation.
**     CA  20230119          Fixed a bug in incremental conversion - already parsed classes must populate the
**                           legacyToJavaMethod map, and keep the javaname of the already converted methods.
** 018 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 019 OM  20230115          Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
**                           versions, based on node types rather on string paths.
** 020 CA  20230519          Overload matching requires to compute the distance between the argument's type
**                           and the parameter's type, such that an int argument will choose the int64 
**                           parameter instead of a decimal parameter.  Also, DATASET/TABLE-HANDLE must allow
**                           matches to continue, so the selection is done on the full signature.
** 021 CA  20230426          For method INPUT arguments, if the argument is from an expression and the arg's
**                           type is resolved as 'int64' for an 'integer' parameter, allow it; the runtime 
**                           will validate the actual values.
**     CA  20230501          In 'annotateCallSignature', an '?' literal for a 'object' parameter must emit as
**                           'new object()'.
** 022 CA  20231004          Fixed conversion for class events with parameters of type DATASET, TABLE, 
**                           DATASET-HANDE, TABLE-HANDLE and BUFFER.
** 023 CA  20230928          The openedge method resolution when the match is done via a poly argument  
**                           (like h::f1), it will assume the return type of the last found overload - the  
**                           runtime will still resolve the target based on the arguments, but the lookup must 
**                           be done on the type resolved by the conversion, and not the runtime time of the 
**                           lvalue on the chain call. For this reason, and to allow the chained call to fail 
**                           properly at runtime, both the lookup type and the return type of the call are 
**                           emitted at the 'invokePoly' API call.
**                           Refactored the fuzzy method resolution to be used by runtime, too.
**     CA  20230929          Method chain calls starting with SUPER keyword can not be morphed to dynamic 
**                           invoke, as the super calls require JVM to handle them.
** 024 CA  20231016          Register the virtual enum methods when loading a referenced enum AST.
** 025 CA  20240601          Added processParentGraph overload to allow processing on descent from parent to
**                           object or on ascent, from object class to direct parent class.
** 026 AOG 20241017          Changed extent parameter value when createMethod is called in registerVirtual.
**     CA  20241026          The parent of .NET enums is System.Enum.
**     CA  20241118          A property 'inherited' from an interface/super-class is allowed to define its
**                           own explicit getter or setter, even if the 'inherited' definition does not define
**                           it.
**     CA  20241118          Annotate method calls with 'dotnet-cls', too, beside 'builtin-cls'.
** 027 CA  20250319          A method from a super-class can have the same name as current class, so calls to 
**                           this kind of methods must not be resolved as a constructor for the current class.
**     CA  20250319          A provisional class var found during pre-scan, which is referenced before the
**                           definition statement and is referenced via a chain, must be resolved even if is
**                           provisional - this is the only case allowed in 4GL, when a var can be defined
**                           after it is referenced.
** 028 AS  20250331          Fixed a typo in exactMethodLookup.
** 029 CA  20250513          Loading a previously parsed class must ensure all the schemas in 'db_references'
**                           are loaded in the SchemaDictionary associated with the ClassDefinition.
** 030 ICP 20250407          Corrected checkAccessRights, added the case when a public method matches a
**                           package private search
**     ICP 20250423          Fixed loadBuiltinDefinition to correctly resolve setter and getter method names.
** 031 CA  20231215          Classes referenced via method parameters need to be loaded for incremental conversion.
** 032 AS  20250605          Resolve inherited properties naming conflicts.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.uast;

import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import java.util.stream.Collectors;

import com.goldencode.ast.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.convert.db.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.Utils; // conflicts with ANTLR so it needs to be explicitly imported

import antlr.*;

/**
 * Contains a dictionary for each type of class resource and provides
 * methods to maintain and lookup in the respective namespaces.
 * <p>
 * Provides support for the following namespaces:
 * <ul>
 *    <li> member variables
 *    <li> properties
 *    <li> methods
 *    <li> temp-tables
 *    <li> buffers
 *    <li> queries
 *    <li> datasets
 *    <li> data-sources
 * </ul>
 */
public class ClassDefinition
implements ProgressParserTokenTypes
{
   /** Logger. */
   private static final ConversionStatus LOG = ConversionStatus.get(ClassDefinition.class);
   
   /** Data store identifier. */
   protected static enum DataStoreType { VAR, METHOD, TABLE, QRY, DATASET, DATASRC };

   /** The converter to use for generating Java names. */
   public static final NameConverter nconvert = new NameConverter();
   
   /** Instance data stores. */
   private Map<DataStoreType, Map<String, MemberData>> istores = null; 
   
   /** Static data stores. */
   private Map<DataStoreType, Map<String, MemberData>> sstores = null;
   
   /** Instance methods mapped by name. */
   private Map<String, Map<SignatureKey, MemberData>> imethods = null; 
   
   /** Static methods mapped by name. */
   private Map<String, Map<SignatureKey, MemberData>> smethods = null;

   /**
    * A set of existing Java signatures to detect when there is a method overloading conflict (more than one
    * method with the same java name and signature).  Some overloaded methods can convert to identical Java
    * signatures when they are different in the 4GL.
    */
   private Set<String> javaMethodCollisions = new HashSet<>();

   /** Lookup of the Java name (known to be unique) from the legacy signature (which also is be unique). */
   private Map<String, String> legacyToJavaMethod = new HashMap<>();
   
   /** Internal lock object. */
   private Object lock = new Object();
   
   /** Provides schema lookup for this class' scope. */
   private SchemaDictionary schemaDict = null;
   
   /** Fully qualified name of the class or interface. */
   private final String name;
   
   /** Super class definition. Only interfaces can have multiple parents. */
   private final ClassDefinition[] parents;
   
   /** Implemented interfaces. */
   private final ClassDefinition[] interfaces;
   
   /** List of implemented interfaces. */
   private final Set<String> ifaces;
   
   /** Flag marking if this is a built-in Progress class. */
   private final boolean builtin;
   
   /** Flag to differentiate between 4GL classes and .NET classes. */
   private final boolean dotnet;
   
   /** Flag marking the type of this OO 4GL compile unit. */
   private final OOType otype;
   
   /** The filename of the .cls that defined this class/interface. */
   private final String filename;
   
   /** Temporary ID number (before AST IDs are assigned). */
   private int tempIdx = 0;
   
   /** Classes for which we've already calculated .NET dependencies. */
   private Set<ClassDefinition> dotnetProcessed = new HashSet<>();
   
   /** .NET classes in the parent hierarchy. */
   private Set<String> dotnetDerived = new HashSet<>();
   
   /** Direct .NET references. */
   private Set<String> directDotNet = new HashSet<>();
   
   /** Indirect .NET references. */
   private Set<String> indirectDotNet = new HashSet<>();
   
   /** Flag indicating that this class was processed (fully parsed) after it was pre-scanned. */
   private boolean processed = false;
   
   /** Full Java package in dotted notation (no class name). */
   private String javaPackage = null;

   /** The Java field names for skeleton enums. */
   private Map<String, String> javaEnumNames = new HashMap<>();
   
   /** Simple Java class name, without any package prefix. */
   private String javaClassName = null;
   
   /** A set of already loaded properties - as a class is parsed during both prescan and full-scan. */
   private Set<String> loadedProperties = new HashSet<>();
   
   /** The fuzzy method lookup. */
   private final FuzzyMethodCvt fuzzy = new FuzzyMethodCvt(this);
   
   /**
    * Construct an instance and load any superclass or interface hierarchy
    * that is needed.  For each superclass or interface, a separate class
    * definition will be loaded. These definitions will be linked back by
    * reference from this instance. This allows lookups to be done based on
    * the object hierarchy.
    *
    * @param    name
    *           Fully qualified name of the class or interface.
    * @param    parents
    *           List of parent classes, only interfaces can have more than 1.  Pass
    *           <code>null</code> if there is no explicit super class.
    * @param    interfaces
    *           Collection of fully qualified interface names that are
    *           implemented by this class.
    * @param    builtin
    *           <code>true</code> if this definition is built-in to Progress.
    * @param    otype
    *           Flag marking the type of this OO 4GL compile unit.
    * @param    dotnet
    *           <code>true</code> if this is a .NET definition,
    *           <code>false</code> if this is a 4GL definition.
    * @param    filename
    *           The filename of the .cls that defined this class/interface.
    */
   public ClassDefinition(String            name,
                          ClassDefinition[] parents,
                          ClassDefinition[] interfaces,
                          boolean           builtin,
                          OOType            otype,
                          boolean           dotnet,
                          String            filename)
   {
      this.name       = name;
      this.parents    = parents;
      this.interfaces = interfaces;
      this.ifaces     = (interfaces != null) ? new HashSet<String>() : null;
      this.builtin    = builtin;
      this.otype      = otype;
      this.dotnet     = dotnet;
      this.filename   = filename;

      if (interfaces != null)
      {
         for (ClassDefinition iface : interfaces)
         {
            ifaces.add(iface.getName());
         }
      }
      
      if (builtin && !dotnet)
      {
         loadBuiltinDefinition();
      }
      
      if (otype == OOType.ENUM && !isJava() && !isDotNet())
      {
         registerVirtualEnumMethods(name, parents[0].getName().equalsIgnoreCase("Progress.Lang.FlagsEnum"));
      }
   }
   
   /**
    * Given a fully qualified legacy class name, load a new definition by looking into the AST for
    * <code>oo-data-member</code> nodes and populate the members.
    * 
    * @param    sym
    *           The symbol resolver.
    * @param    qname
    *           The qualified legacy name for the class.
    *           
    * @throws   SchemaException
    *           If temp-tables from this definition couldn't be loaded.
    */
   public ClassDefinition(SymbolResolver sym, String qname)
   throws SchemaException
   {
      this.processed = true;
      this.name = qname;
      this.filename = ConversionData.findClassFilename(qname);
      
      // load the root AST and load the following:
      // parents (1 for class, 1-n for iface)
      // interfaces (0-n only for class)
      Aast root = AstManager.get().loadTree(filename + ".ast");
      
      // get from the root
      this.dotnet = Boolean.TRUE.equals(root.getAnnotation("dotnet-cls"));
      this.builtin = Boolean.TRUE.equals(root.getAnnotation("builtin-cls")) && !dotnet;
      this.otype = root.downPath(INTERFACE_DEF) ? OOType.INTERFACE
                                                : root.downPath(ENUM_DEF) ? OOType.ENUM : OOType.CLASS;
      if (builtin && !dotnet)
      {
         // this must be before we walk the AST
         loadBuiltinDefinition();
      }
      
      if (!builtin)
      {
         this.schemaDict = new SchemaDictionary(null);
         List<Object> dblist = root.getAnnotationList("db_references");
         if (dblist != null)
         {
            for (Object odb : dblist)
            {
               String db = odb.toString();
               if (!schemaDict.isDatabase(db))
               {
                  schemaDict.loadSchema(db);
               }
            }
         }
         
         this.schemaDict.addScope(false);
         SchemaWorker.loadFromPersistence(schemaDict, filename);
         
         this.javaPackage   = (String) root.getAnnotation("pkgname");
         this.javaClassName = (String) root.getAnnotation("classname");
      }
      
      Set<String>           ifaces     = new HashSet<>();
      List<ClassDefinition> interfaces = new ArrayList<>();
      List<ClassDefinition> parents    = new ArrayList<>();
      boolean hadRootClass = false;

      BiConsumer<List<ClassDefinition>, String> resolveParent = (list, pqname) ->
      {
         ClassDefinition clsdef = SymbolResolver.loadClassDefinition(sym, pqname);
         if (clsdef == null)
         {
            if (sym == null)
            {
               // this code (and actually this ClassDefinition c'tor) should never be reached with a null
               // SymbolResolver.
               String file = ConversionData.findClassFilename(qname);
               file = file + ".ast";
               throw new RuntimeException("Can't load parent class " + pqname + 
                                          " - symbol resolver not available and " + file + " does not exist.");
            }

            String foundName = sym.loadClass(pqname);
            clsdef = sym.lookupClass(foundName);
         }
         
         if (clsdef == null)
         {
            throw new RuntimeException("Could not load parent class: " + pqname);
         }
         
         list.add(clsdef);
      };
      
      boolean isIface = this.otype == OOType.INTERFACE;
      Iterator<Aast> iter = root.iterator();
      Aast ref = null;
      String pqname;
      while (iter.hasNext())
      {
         Aast node = iter.next();
         int nodeType = node.getType();
         
         switch (nodeType)
         {
            // parents and interfaces
            case KW_IMPLEMTS:
               ref = node.getImmediateChild(CLASS_NAME, null);
               pqname = (String) ref.getAnnotation("qualified");

               if (isIface)
               {
                  resolveParent.accept(parents, pqname);
               }
               else
               {
                  resolveParent.accept(interfaces, pqname);
                  ifaces.add(pqname);
               }
               break;

            case KW_INHERITS:
               ref = node.getImmediateChild(CLASS_NAME, null);
               pqname = (String) ref.getAnnotation("qualified");
               resolveParent.accept(parents, pqname);
               hadRootClass = SymbolResolver.ROOT_OBJ_NAME.equalsIgnoreCase(pqname);
               break;
         }
         
         // members
         if (node.isAnnotation("oo-data-store"))
         {
            String storeName = (String) node.getAnnotation("oo-data-store");
            DataStoreType storeType = DataStoreType.valueOf(storeName);
            boolean isStatic = Boolean.TRUE.equals(node.getAnnotation("static"));
            boolean isOverride = Boolean.TRUE.equals(node.getAnnotation("override"));
            
            if (storeType == DataStoreType.METHOD)
            {
               String         name = (String) node.getAnnotation("name");
               ParameterKey[] sig  = SymbolResolver.calculateDefinitionSignature(node);
               
               MemberData mdat = createMethod(null,
                                              name,
                                              sig, 
                                              ((Long) node.getAnnotation("access-mode")).intValue(),
                                              isStatic,
                                              ((Long) node.getAnnotation("return_type")).intValue(),
                                              (String) node.getAnnotation("qualified"),
                                              (String) node.getAnnotation("javaname"),
                                              SymbolResolver.readExtent(node),
                                              -1,
                                              node.getImmediateChild(KW_OVERRIDE, null) != null,
                                              node.getImmediateChild(KW_ABSTRACT, null) != null);
               
               for (int i = 0; i < mdat.signature.length; i++)
               {
                  ParameterKey p = mdat.signature[i];
                  if (p.type.startsWith("object<"))
                  {
                     String psig = "object<? extends ";
                     int    pidx = p.type.indexOf(psig);
                     if (pidx != -1)
                     {
                        String pcname = p.type.substring(pidx + psig.length(), p.type.length() - 1);
                        if (!pcname.equalsIgnoreCase(this.name))
                        {
                           SymbolResolver.loadClassDefinition(sym, pcname, false);
                        }
                     }
                  }
               }
               
               if (!builtin)
               {
                  mdat.astId = node.getId();
                  mdat.tempIdx = ConversionData.getTempIdx(mdat.astId);
                  mdat.javaname = (String) node.getAnnotation("javaname");
                  
                  registerMethod(mdat.javaname, mdat);
               }
            }
            else
            {
               int tokenType = -1;
               String varName = (String) node.getAnnotation("name");
               Variable var;
               int accessMode = node.isAnnotation("access-mode") 
                                  ? ((Long) node.getAnnotation("access-mode")).intValue()
                                  : ProgressParserTokenTypes.KW_PRIVATE;
               
               switch (storeType)
               {
                  case DATASET:
                     tokenType = DATA_SET;
                     var = SymbolResolver.createVariable(varName, tokenType, node, isStatic);
                     break;
                     
                  case DATASRC:
                     tokenType = DATA_SOURCE;
                     var = SymbolResolver.createVariable(varName, tokenType, node, isStatic);
                     break;
                     
                  case QRY:
                     var = null;
                     tokenType = QUERY;
                     break;
                  
                  case TABLE:
                     var = null;
                     if (nodeType == DEFINE_BUFFER)
                     {
                        tokenType = BUFFER;
                        Aast symNode = node.getImmediateChild(SYMBOL, null);
                        String bufName = symNode.getText();
                        
                        node = node.getImmediateChild(KW_FOR, null);
                        boolean skipGlobal = node.downPath(TEMP_TABLE);
                        if (node.downPath(TEMP_TABLE))
                        {
                           node = node.getImmediateChild(TEMP_TABLE, null);
                        }
                        else
                        {
                           node = node.getImmediateChild(TABLE, null);
                        }
                        varName = (String) node.getAnnotation("bufname");
                        String tableName = (String) node.getAnnotation("schemaname");
                        
                        Object table = schemaDict.addTableEntry(null, 
                                                                bufName, 
                                                                BUFFER, 
                                                                true, 
                                                                symNode);
                        NameNode toTable = (NameNode) table;
                        NameNode fromTable = schemaDict.findTableFromNode(tableName, toTable, skipGlobal);
                        schemaDict.addFieldEntries(tableName, fromTable, toTable, skipGlobal);
                     }
                     else if (nodeType == KW_B4_TABLE)
                     {
                        Aast symNode = node.getImmediateChild(SYMBOL, null);
                        varName = symNode.getText();
                     }
                     else
                     {
                        varName = (String) node.getAnnotation("bufname");
                        if (node.downPath(KW_WORK_TAB))
                        {
                           tokenType = WORK_TABLE;
                        }
                        else
                        {
                           tokenType = TEMP_TABLE;
                        }
                        
                        /*
                        Aast ttNode;
                        ttNode = node.getImmediateChild(KW_WORK_TAB, null);
                        ttNode = node.getImmediateChild(KW_TEMP_TAB, null);
                        NameNode table = schemaDict.addTableEntry(null, 
                                                                  varName, 
                                                                  tokenType, 
                                                                  true, 
                                                                  ttNode);
                        Iterator<Aast> fiter = ttNode.iterator();
                        while (iter.hasNext())
                        {
                           Aast field = fiter.next();
                           if (field.getType() != DEFINE_FIELD)
                           {
                              continue;
                           }
                           schemaDict.addFieldEntry(table, node, field, ftype, true);
                           sym.addField(table, symName, ftype, as_field_clause_AST);
                           // ?? sym.addFieldLike(table, symName, l_AST)
                        }
                        */
                     }
                  break;
                     
                  case VAR:
                     if (nodeType == DEFINE_VARIABLE || nodeType == DEFINE_PROPERTY)
                     {
                        tokenType = ((Long) node.getAnnotation("type")).intValue();
                        var = new Variable(varName, 
                                           tokenType, 
                                           (String) node.getAnnotation("qualified"));
                        var.setDotNetArray(Boolean.TRUE.equals(node.getAnnotation("dotnet-array")));
                        
                        if (node.isAnnotation("generic-type-parameter"))
                        {
                           var.setGenericType((String) node.getAnnotation("generic-type-parameter"));
                        }
                        if (node.isAnnotation("generic-type-is-primitive"))
                        {
                           boolean prim = (boolean) node.getAnnotation("generic-type-is-primitive");
                           var.setGenericPrimitive(prim);
                        }
                     }
                     else
                     {
                        // enum or event
                        tokenType = nodeType == DEFINE_EVENT ? CLASS_EVENT : VAR_CLASS;
                        var = new Variable(varName,
                                           tokenType, 
                                           (String) node.getAnnotation("qualified"));
                        tokenType = VAR_CLASS;
                        
                        // TODO: for enums, do we need to lookup the javaname in javaEnumNames here or
                        //       can we rely upon the AST node's "javaname" annotation?
                     }
                     
                     var.setProperty(nodeType == DEFINE_PROPERTY);
                     var.setStatic(isStatic && !isEnum());
                     var.setAccess(accessMode);
                     var.setExtent(SymbolResolver.readExtent(node));
                     var.setOverride(isOverride);
                     
                     // we need to know the variable's state at this point
                     SymbolResolver.trackPropertyOrEventMethodSignatures(nodeType, this, node, var, varName);
                     break;
                     
                   default:
                      throw new RuntimeException("Unknown member for oo-data-store:\n" + 
                                                 node.dumpTree(true));
               }
               
               MemberData mdat = addWorker(storeType,
                                           varName,
                                           null,
                                           var,
                                           tokenType,
                                           accessMode,
                                           isStatic,
                                           (String) node.getAnnotation("qualified"),
                                           -1);
               mdat.javaname = (String) node.getAnnotation("javaname");
               if (var != null)
               {
                  var.setClassDefinition(this);
                  var.setBuiltin(this.builtin);
                  var.setOptions(node);
               }
               if (!builtin)
               {
                  mdat.astId = node.getId();
                  mdat.tempIdx = ConversionData.getTempIdx(mdat.astId);
                  if (var != null)
                  {
                     var.setTempIndex(mdat.tempIdx);
                  }
               }
            }
         };
      }
      
      // TODO: enum root class
      // TODO: register GetEnum methods
      
      if (!hadRootClass && !qname.equalsIgnoreCase(SymbolResolver.ROOT_OBJ_NAME))
      {
         ClassDefinition parent = SymbolResolver.loadClassDefinition(sym, SymbolResolver.ROOT_OBJ_NAME);
         if (parent == null)
         {
            throw new RuntimeException("Could not load parent class definition " + 
                                       SymbolResolver.ROOT_OBJ_NAME + " for " + qname);
         }
         parents.add(parent);
      }
      
      this.ifaces     = ifaces.isEmpty() ? null : Collections.unmodifiableSet(ifaces);
      this.interfaces = interfaces.isEmpty() ? null : interfaces.toArray(new ClassDefinition[0]);
      this.parents    = parents.isEmpty() ? null : parents.toArray(new ClassDefinition[0]);

      if (otype == OOType.ENUM && !isJava())
      {
         registerVirtualEnumMethods(name, this.parents[0].getName() == "Progress.Lang.FlagsEnum");
      }
   }
   
   /**
    * Detect if the signature contains a polymorphic type (is {@code BaseDataType}).
    *
    * @param    parms
    *           Parameter array.
    *
    * @return   {@code true} if a parameter is polymorphic.
    */
   public static boolean isPolySignature(ParameterKey[] parms)
   {
      for (ParameterKey p : parms)
      {
         if ("BaseDataType".equals(p.type))
         {
            return true;
         }
      }
      
      return false;
   }
   
   /**
    * Lookup or calculate the Java method name for a property or event and store the data for method
    * name disambiguation. 
    * 
    * @param    node
    *           The AST for which the method is being registered.
    * @param    prefix
    *           The accessor prefix.
    * @param    lname
    *           The legacy property/event name.
    * @param    override
    *           {@code true} if the property or event is overridden.
    * @param    abstr
    *           {@code true} if the property or event is abstract.
    * @param    sig
    *           The parameter definition.
    *           
    * @return   The Java method name that is associated with the specific property accessor.
    */
   public String registerPropertyMethod(Aast           node,
                                        String         prefix, 
                                        String         lname, 
                                        boolean        override,
                                        boolean        abstr,
                                        ParameterKey[] sig)
   {
      String  accessor  = prefix + "-" + lname;
      String  legacySig = SymbolResolver.calculateLegacySignature(accessor, sig);
      String  jparms    = SymbolResolver.calculateJavaParameters(sig);
      String  key       = prefix + "-javaname";
      String  javaname  = null;
      
      if (!isBuiltIn() && loadedProperties.contains(legacySig))
      {
         javaname = getConvertedMethodName(legacySig);
         node.putAnnotation(key, javaname);
         
         return javaname;
      }
      
      loadedProperties.add(legacySig);

      if (node.isAnnotation(key))
      {
         // subsequent time in, read the stored name from the AST and update the in memory storage
         javaname = (String) node.getAnnotation(key);
         
         addJavaMethodName(javaname, jparms, legacySig);
      }
      else if (isBuiltIn())
      {
         javaname = getConvertedMethodName(legacySig);
         if (javaname != null)
         {
            addJavaMethodName(javaname, jparms, legacySig);

            node.putAnnotation(key, javaname);
         }
         else
         {
            LOG.log(Level.WARNING, 
                    "Could not find legacy builtin method javaname for '" + name + ":" + legacySig + "'");
         }
      }
      else if (override && (javaname = lookupOverrideName(legacySig, override, true)) != null)
      {
         // first time in (for an override)
         addJavaMethodName(javaname, jparms, legacySig);

         node.putAnnotation(key, javaname);
      }
      else
      {
         if (abstr)
         {
            // abstract properties/events that match an interface property/event should get the same name
            javaname = lookupOverrideName(legacySig, false, abstr);
            
            if (javaname != null)
            {
               addJavaMethodName(javaname, jparms, legacySig);
            }
         }

         if (javaname == null)
         {
            // first time in (for a non-override), calculate the name and store it in memory and in the AST
            javaname = calculateUniqueJavaMethodName(accessor, jparms, legacySig);
         }
         
         node.putAnnotation(key, javaname);
      }

      return javaname;
   }
   
   /**
    * Compute the Java name for a legacy method which when combined with the Java parameter list will
    * be a unique method name.
    * 
    * @param    name
    *           The legacy name.
    * @param    jparms
    *           The Java parameter list definition.
    * @param    legacySig
    *           The legacy signature that will be mapped to the Java name.
    *           
    * @return   The Java name.
    */
   private String calculateUniqueJavaMethodName(String name, String jparms, String legacySig)
   {
      String jbase   = nconvert.convert(name, MatchPhraseConstants.TYPE_METHOD);
      String jname   = jbase;
      String javaSig = null;
      
      // TODO: the suffix can be replaced with something more verbose, to indicate i.e. the
      // collision reason
      int suffix = 1;
      
      while (true)
      {
         javaSig = jname + "(" + jparms + ")"; 
         
         if (!hasCollision(javaSig))
         {
            addJavaMethodName(jname, jparms, legacySig);
            break;
         }
         
         jname = jbase + "_" + suffix;
         suffix++;
      }

      return jname;
   }
   
   /**
    * Check the current class and all parent classes/interfaces for a Java signature collision.
    *  
    * @param    javaSig
    *           The Java method signature for which to check.
    *           
    * @return   {@code true} if there is a collision (an existing registered method with the same Java
    *           signature).
    */
   private boolean hasCollision(String javaSig)
   {
      // quick out, fail if there is a local collision
      if (javaMethodCollisions.contains(javaSig))
      {
         return true;
      }
      
      java.util.function.Function<ClassDefinition, Boolean> collide = (ClassDefinition cls) ->
      {
         return cls.javaMethodCollisions.contains(javaSig);
      };

      // no local collision, but now we need to check our parent hierarchy
      return calcFromParentGraph(collide, (Boolean value) -> !value, Boolean.FALSE, true, true);
   }

   /**
    * Update the internal maps to record a new Java name.
    * 
    * @param    javaname
    *           The Java method name to add.
    * @param    jparms
    *           The Java parameter list definition.
    * @param    legacySig
    *           The legacy signature that will be mapped to the Java name.
    */
   private void addJavaMethodName(String javaname, String jparms, String legacySig)
   {
      String javaSig = javaname + "(" + jparms + ")";

      if (!javaMethodCollisions.contains(javaSig))
      {
         javaMethodCollisions.add(javaSig);
      }
      else
      {
         // don't uncomment this except for debugging; built-in classes (and others?) will trigger this
         // System.err.printf("ERROR: Java signature %s already exists in collisions list.\n", javaSig);
      }
      
      String old = legacyToJavaMethod.put(legacySig, javaname);
      
      if (old != null && !old.equals(javaname))
      {
         // something is wrong... name must match!
         LOG.log(Level.SEVERE, 
                 "method " + legacySig +
                    " accessible from class " + getName() +
                    " has two Java names: " + javaname + " and " + old);
      }
   }

   /**
    * Register this method signature with a converted Java method name.  The member data structure will
    * have its javaname member set as a result of this call.  4 cases are handled (in this order):
    * <p>
    * <ol>
    *    <li> The caller has passed in a non-null javaname.  This will force the name.
    *    <li> The method is a overridden method, lookup the method in the parent class and use that name.
    *    <li> The method signature is already registered, use the registered name.
    *    <li> Calculate a unique name and register it.
    * </ol>
    * 
    * @param    javaname
    *           The javaname to forcibly assign to this legacy signature, or <code>null</code> to
    *           compute an unique one.
    * @param    mdat
    *           The method definition.  Must NOT be {@code null}.
    */
   private void registerMethod(String javaname, MemberData mdat)
   {
      String jparms    = SymbolResolver.calculateJavaParameters(mdat.signature);
      String legacySig = SymbolResolver.calculateLegacySignature(mdat.name, mdat.signature);
      
      // check if any interface parents has a legacy signature for this method - as no OVERRIDE is required
      // when implementing or re-defining as abstract these interface methods
      if (!mdat.override)
      {
         mdat.override = lookupOverrideName(legacySig, true, true) != null;
      }
      if (mdat.var != null)
      {
         mdat.var.setOverride(mdat.override);
      }
      
      // case 1: the caller knows best, if javaname is passed then force it here
      if (javaname != null)
      {
         mdat.javaname = javaname;
         addJavaMethodName(mdat.javaname, jparms, legacySig);         
      }
      
      // case 2: overrides use the name of the parent (inheritance hierarchy)
      else if (mdat.override)
      {
         mdat.javaname = lookupOverrideName(legacySig, mdat.override, true);
         addJavaMethodName(mdat.javaname, jparms, legacySig);
      }
      
      // case 3: the method is already registered with a unique name
      else if (legacyToJavaMethod.containsKey(legacySig))
      {
         mdat.javaname = legacyToJavaMethod.get(legacySig);
      }
      
      else
      {
         if (mdat.abstr)
         {
            // case 4: abstract methods that match an interface method should get the same name
            mdat.javaname = lookupOverrideName(legacySig, false, mdat.abstr);
            
            if (mdat.javaname != null)
            {
               addJavaMethodName(mdat.javaname, jparms, legacySig);
            }
         }

         if (mdat.javaname == null)
         {
            // case 5: not registered, calculate a unique name, avoiding signature conflicts
            mdat.javaname = calculateUniqueJavaMethodName(mdat.name, jparms, legacySig);
         }
      }
   }
   
   /**
    * Lookup the Java method name in the parent/interface hierarchy for the given legacy signature.  The
    * first match is returned.
    *  
    * @param    legacySig
    *           The legacy signature.  This should uniquely identify a single method.
    * @param    override
    *           {@code true} is the method name is an override.  This will force a search of parent
    *           (inheritance hierarchy).
    * @param    abstr
    *           {@code true} is the method name is abstract. This will force a search of any implemented
    *           interfaces.
    *           
    * @return   The first Java name found for this legacy signature or {@code null} if there is no match.
    */
   private String lookupOverrideName(final String legacySig, boolean override, boolean abstr)
   {
      java.util.function.Function<ClassDefinition, String> oride = (ClassDefinition cls) ->
      {
         return cls.legacyToJavaMethod.get(legacySig);
      };
      
      return calcFromParentGraph(oride, (String value) -> value == null, null, override, abstr);
   }
   
   /**
    * Load the FWD implementation for a builtin, non-.NET class.  This maps legacy signatures to
    * Java method names, so that they will be reused for method references or override cases.
    */
   private void loadBuiltinDefinition()
   {
      Class<? extends _BaseObject_> cls = null;

      String fullname = SymbolResolver.findConvertedLegacyClass(name);
      
      if (fullname == null)
      {
         String clsPkg = (name.indexOf(".") < 0 ? "" : name.substring(0, name.lastIndexOf(".")));
         String clsName = clsPkg.isEmpty() ? name : name.substring(clsPkg.length() + 1);
         
         if (clsPkg.toLowerCase().startsWith("openedge") || 
             clsPkg.toLowerCase().startsWith("progress"))
         {
            clsPkg = clsPkg.substring(9);
         }
         if (clsPkg.toLowerCase().startsWith("ccs"))
         {
            clsPkg = clsPkg.substring(4);
         }
         clsPkg = NameConverter.convertPackage(clsPkg);
         clsName = nconvert.convert(clsName, MatchPhraseConstants.TYPE_CLASS);
   
         String fname = clsPkg + (clsPkg.isEmpty() ? "" : ".") + clsName;
         if ("core.string".equalsIgnoreCase(fname))
         {
            clsName = "LegacyString";
         }
         else if ("lang.object".equalsIgnoreCase(fname))
         {
            clsName = "BaseObject";
         }
         else if ("lang.class".equalsIgnoreCase(fname))
         {
            clsName = "LegacyClass";
         }
         else if ("lang.error".equalsIgnoreCase(fname))
         {
            clsName = "LegacyError";
         }
      
         javaPackage   = "com.goldencode.p2j.oo." + clsPkg;
         javaClassName = clsName;
         
         fullname = javaPackage + "." + javaClassName;

         try
         {
            cls = (Class<? extends _BaseObject_>) Class.forName(fullname);
         }
         catch (ClassNotFoundException e)
         {
            LOG.log(Level.SEVERE, "Builtin class " + fullname + " is not implemented in FWD!");
            return;
         }
      }
      else
      {
         try
         {
            cls = (Class<? extends _BaseObject_>) Class.forName(fullname);
            
            int dotIdx = fullname.lastIndexOf('.');
            
            javaPackage   = dotIdx < 0 ? "" : fullname.substring(0, dotIdx);
            javaClassName = dotIdx < 0 ? fullname : fullname.substring(dotIdx + 1);            
         }
         catch (ClassNotFoundException e)
         {
            LOG.log(Level.SEVERE, "Builtin class " + fullname + " is not implemented in FWD!");
            return;
         }
      }
      
      final Class<?> clz = cls;
      Consumer<Class<? extends _BaseObject_>> processMethods = (clazz) ->
      {
         Method[] mthds = clazz.getDeclaredMethods();
         for (Method mthd : mthds)
         {
            LegacySignature lsig = mthd.getAnnotation(LegacySignature.class);
            if (lsig == null || lsig.type() == InternalEntry.Type.DESTRUCTOR)
            {
               continue;
            }
            
            String mname = lsig.name();
            switch (lsig.type())
            {
               // WARNING: if you track a constructor name here, the constructor name i.e. LogMessage 
               // can collide with a method name LogMessage. There is nothing to distinguish a c'tor 
               // from a method, at this time.  In any case, a c'tor can't collide with a method name, 
               // as the c'tor Java name will be a method with a name following some specific rules.
               // Only way for these to collide is for the user to explicitly define a 4GL method 
               // following FWD's c'tor convention for converted Java method name.
               case GETTER:
                  if (lsig.extent() != SourceNameMapper.NO_EXTENT &&
                      lsig.parameters() != null                   &&
                      lsig.parameters().length == 0)
                  {
                     mname = "bulk-get-" + mname;
                  }
                  else
                  {
                     mname = "get-" + mname;
                  }
                  break;

               case SETTER:
                  if (lsig.extent() != SourceNameMapper.NO_EXTENT && lsig.parameters() != null)
                  {
                     if (lsig.parameters().length == 1)
                     {
                        if (lsig.parameters()[0].extent() != SourceNameMapper.NO_EXTENT)
                        {
                           mname = "bulk-set-" + mname;
                        }
                        else
                        {
                           mname = "set-all-" + mname;
                        }
                     }
                  }
                  else
                  {
                     mname = "set-" + mname;
                  }
                  break;

               case RESIZE:
               case LENGTH:
               case PUBLISH:
               case SUBSCRIBE:
               case UNSUBSCRIBE:
                  mname = lsig.type().toString() + "-" + mname;
                  break;
                  
               case CONSTRUCTOR:
                  if (clazz != clz)
                  {
                     // c'tors only from the current definition
                     continue;
                  }
                  String qname = getSimpleJavaName();
                  String qpkg = getJavaPackage().substring("com.goldencode.p2j.oo.".length());
                  qname = qpkg + "." + qname;
                  qname = qname.replace('.', '_');
                  mname = String.format("__%s_constructor_", qname);
                  if (Modifier.isStatic(mthd.getModifiers()))
                  {
                     mname = String.format("%sstatic__", mname);
                  }
                  break;
            }
            String legacySig = SymbolResolver.calculateLegacySignature(mname, lsig);
            String javaname  = mthd.getName();
            
            addJavaMethodName(javaname, "", legacySig);
         }
      };

      if (LegacyEnum.class.isAssignableFrom(cls))
      {
         Field[] fields = cls.getDeclaredFields();
         for (int i = 0; i < fields.length; i++)
         {
            Field field = fields[i];
            int mods = field.getModifiers();
            if (!(Modifier.isPublic(mods) && Modifier.isStatic(mods)))
            {
               continue;
            }
            
            LegacySignature s = field.getAnnotation(LegacySignature.class);
            if (s == null)
            {
               LOG.log(Level.SEVERE, "LegacySignature annotation is missing for enum field " + field + "!");
               continue;
            }
            if (s.type() != InternalEntry.Type.VARIABLE)
            {
               LOG.log(Level.SEVERE,
                       "LegacySignature annotation at enum field " + field + " is not a variable!");
               continue;
            }
            
            String legacy = s.name().toLowerCase();
            String javaname = field.getName();
            
            javaEnumNames.put(legacy, javaname);
         }
      }
      
      Class<?> parent = cls;
      while (parent != null)
      {
         processMethods.accept((Class<? extends _BaseObject_>) parent);
         parent = parent.getSuperclass();

         if (parent == Object.class || parent == BaseObject.class)
         {
            parent = null;
         }
      }
      
      Set<Class<?>> ifaces = new HashSet<>();
      Utils.collectInterfaces(cls, ifaces);
      ifaces.forEach((iface) -> processMethods.accept((Class<? extends _BaseObject_>) iface));
   }
   
   /**
    * Get the converted java method name for a method - this ensures the same Java name is used 
    * for methods which are overridden, across the entire converted legacy classes.
    * <p>
    * Legacy methods for which there are collisions with the converted Java signatures, the names
    * are made unique, but still ensures that any overridden versions will reuse the same name.
    * 
    * @param    legacy
    *           The legacy name.
    * @param    mAst
    *           The method definition AST.
    *           
    * @return   The converted Java name to use for this legacy method.
    */
   public String getConvertedMethodName(String legacy, Aast mAst)
   {
      ParameterKey[] signature = SymbolResolver.calculateDefinitionSignature(mAst);
      return getConvertedMethodName(SymbolResolver.calculateLegacySignature(legacy, signature));
   }

   /**
    * Get the Java method name for a legacy signature.  This signature may be from a super-class.
    * 
    * @param    legacySig
    *           The legacy signature.
    *
    * @return   The converted method name or {@code null} if no match is found.
    */
   public String getConvertedMethodName(String legacySig)
   {
      if (legacyToJavaMethod.get(legacySig) != null)
      {
         return legacyToJavaMethod.get(legacySig);
      }
      
      if (parents != null)
      {
         for (ClassDefinition parent : parents)
         {
            String result = parent.getConvertedMethodName(legacySig);
            
            if (result != null)
            {
               return result;
            }
         }
      }

      return null;
   }

   /**
    * Set the fully parse state of this class.
    * 
    * @param    state
    *           The processed state.
    */
   public void setProcessed(boolean state)
   {
      this.processed = state;
   }
   
   /**
    * Check if this class definition was already fully parsed.
    * 
    * @return   The {@link #processed} flag.
    */
   public boolean isProcessed()
   {
      return processed;
   }
   
   /**
    * Check the parent hierarchy to determine if the 4GL class has a .NET ancestor. Store
    * the result for future use.  This just reports the indirect .NET dependency and does
    * not report if this is itself a .NET class.
    */
   public void derivedFromDotNet()
   {
      if (!dotnet && parents != null)
      {
         for (ClassDefinition parent : parents)
         {
            if (parent.dotnet)
            {
               dotnetDerived.add(parent.getName());
            }
            
            // recursively process up the inheritance hierarchy
            parent.derivedFromDotNet();
            
            // add those classes to the set if needed
            if (parent.isDerivedFromDotNet())
            {
               dotnetDerived.addAll(parent.dotnetDerived);
            }
         }
      }
   }
   
   /**
    * Report if the parent hierarchy includes a .NET ancestor. This just reports the indirect
    * .NET dependency and does not report if this is itself a .NET class.
    *
    * @return   {@code true} if an ancestor is a .NET class. 
    */
   public boolean isDerivedFromDotNet()
   {
      return dotnetDerived.size() > 0;
   }
   
   /**
    * Obtains the derived (parent hierarchy) .NET references, if any.
    *
    * @return   The derived (parent hierarchy) .NET references.
    */
   public Set<String> getDerivedDotNet()
   {
      return new HashSet<String>(dotnetDerived);
   }
   
   /**
    * Calculate if the given reference is a direct or indirect .NET reference and update out
    * counters.
    *
    * @param    ref
    *           The class being referenced.
    */
   public void markDotNetUsage(ClassDefinition ref)
   {
      // quick exit if we've already processed this class reference
      if (dotnetProcessed.contains(ref))
      {
         return;
      }
      else
      {
         dotnetProcessed.add(ref);
      }
      
      // this is a direct .NET reference
      if (ref.isDotNet())
      {
         directDotNet.add(ref.getName());
      }
      else
      {
         boolean indirect = false;
         
         // this reference is not a direct .NET reference but the reference
         // may have direct or indirect references, both of which are considered
         // indirect references here
         
         if (ref.isDerivedFromDotNet())
         {
            // add the list of parent .NET classes to our own list of indirect
            indirectDotNet.addAll(ref.dotnetDerived);
            indirect = true;
         }
         
         if (ref.hasDirectDotNet())
         {
            // add the list of direct references to our own list of indirect
            indirectDotNet.addAll(ref.directDotNet);
            indirect = true;
         }
         
         if (ref.hasIndirectDotNet())
         {
            // add the list of indirect references to our own list
            indirectDotNet.addAll(ref.indirectDotNet);
            indirect = true;
         }
         
         // were there any indirect references inside this referenced class?
         if (indirect)
         {
            // then add this class name itself to the list of indirect references
            indirectDotNet.add(ref.getName());
         }
      }
   }
   
   /**
    * Reports if the class has any direct .NET references.
    *
    * @return   {@code true} if the class has any direct .NET references.
    */
   public boolean hasDirectDotNet()
   {
      return directDotNet.size() > 0;
   }
   
   /**
    * Obtains the direct .NET references, if any.
    *
    * @return   The direct .NET references.
    */
   public Set<String> getDirectDotNet()
   {
      return new HashSet<String>(directDotNet);
   }
   
   /**
    * Reports if the class has any indirect .NET references.
    *
    * @return   {@code true} if the class has any indirect .NET references.
    */
   public boolean hasIndirectDotNet()
   {
      return indirectDotNet.size() > 0;
   }
   
   /**
    * Obtains the indirect .NET references, if any.
    *
    * @return   The indirect .NET references.
    */
   public Set<String> getIndirectDotNet()
   {
      return new HashSet<String>(indirectDotNet);
   }
   
   /**
    * Reports if the a reference to this class should be considered an indirect .NET reference
    * in the calling class.
    *
    * @return   {@code true} if the class is NOT a .NET class AND it is derived from a .NET
    *           class or has its own direct or indirect references to a .NET class.
    */
   public boolean isIndirectDotNetReference()
   {
      return !dotnet && (isDerivedFromDotNet() || hasDirectDotNet() || hasIndirectDotNet());
   }
   
   /**
    * Get the AST IDs for all static or instance table definitions.
    *  
    * @return   A set of all static or instance table definitions, having protected access mode.
    */
   public Set<Long> getTableIds()
   {
      // only protected tables are seen - there is no public access mode for them.
      Map<String, MemberData> staticTables = getMemberStore(DataStoreType.TABLE, true);
      Map<String, MemberData> instanceTables = getMemberStore(DataStoreType.TABLE, false);
      
      Set<Long> res = new HashSet<>();
      if (staticTables != null)
      {
         staticTables.values().forEach(mdat -> 
         {
            if (!mdat.provisional && mdat.access == KW_PROTECTD)
            {
               res.add(mdat.astId);
            }
         });
      }
      if (instanceTables != null)
      {
         instanceTables.values().forEach(mdat -> 
         {
            if (!mdat.provisional && mdat.access == KW_PROTECTD)
            {
               res.add(mdat.astId);
            }
         });
      }

      return res;
   }
   
   /**
    * The parse for this entire file has finished, and the AST IDs have been computed.  
    * This is the time the capture the AST IDs or add other annotations, as is executed just
    * before the AST is persisted.
    * 
    * @param    global
    *           Flag indicating that the parsing of all files has finished, and a global cleanup can be done 
    *           for this instance.
    */
   public void parseFinished(boolean global)
   {
      if (imethods != null)
      {
         imethods.keySet().forEach(k -> imethods.get(k).values().forEach(md -> md.parseFinished(global)));
      }
      if (smethods != null)
      {
         smethods.keySet().forEach(k -> smethods.get(k).values().forEach(md -> md.parseFinished(global)));
      }
      if (istores != null)
      {
         istores.keySet().forEach(k -> istores.get(k).values().forEach(md -> md.parseFinished(global)));
      }
      if (sstores != null)
      {
         sstores.keySet().forEach(k -> sstores.get(k).values().forEach(md -> md.parseFinished(global)));
      }
      
      if (global)
      {
         schemaDict = null;
      }
      else
      {
         cleanupObjectResources();
      }
   }
   
   /**
    * Obtains the name of this class.
    *
    * @return   Fully qualified class name.  
    */
   public String getName()
   {
      return name;
   }
   
   /**
    * Get the simple legacy name for this class.
    * 
    * @return   See above.
    */
   public String getSimpleName()
   {
      if (name == null)
      {
         return null; // not yet computed
      }
      
      int dotIdx = name.lastIndexOf('.');
      return dotIdx < 0 ? name : name.substring(dotIdx + 1);
   }

   /**
    * Set the converted Java package and Java class name.
    * 
    * @param    pkg
    *           Full Java package in dotted notation (no class name).
    * @param    cls
    *           Simple Java class name, without any package prefix.
    */
   public void setJavaClassName(String pkg, String cls)
   {
      this.javaPackage   = pkg;
      this.javaClassName = cls;
   }
   
   /**
    * Get the converted fully qualified Java class name for this definition.
    * 
    * @return   The converted fully qualified Java class name or {@code null} if it has not yet been
    *           computed.
    */
   public String getQualifiedJavaName()
   {
      String clsName = getSimpleJavaName();
      
      if (clsName == null)
      {
         return null;
      }
      
      String pkg = getJavaPackage();
      
      return (pkg == null || pkg.isEmpty()) ? clsName : pkg + "." + clsName; 
   }
   
   /**
    * Get the converted simple Java class name for this definition.
    * 
    * @return   The converted Java class name or {@code null} if it has not yet been computed.
    */
   public String getSimpleJavaName()
   {
      return javaClassName;
   }
   
   /**
    * Get the converted Java package for this definition.
    * 
    * @return   The converted Java package name or {@code null} if it has not yet been computed.
    */
   public String getJavaPackage()
   {
      return javaPackage;
   }
   
   /**
    * Obtains the filename of the .cls that defined this class/interface.
    *
    * @return   Project relative .cls source file name.  
    */
   public String getFilename()
   {
      return filename;
   }
   
   /**
    * Obtains the parent class definition for this class.
    *
    * @return   Parent class definition or <code>null</code> if there is no
    *           explicit parent.
    */
   public ClassDefinition[] getParents()
   {
      return parents;
   }
   
   /**
    * Obtains the list of interfaces implemented by this class.
    *
    * @return   Implemented interfaces.
    */
   public Set<String> getInterfaces()
   {
      return ifaces == null ? null : new HashSet<String>(ifaces);
   }
   
   /**
    * Access the flag that denotes if this is a built-in class definition.
    *
    * @return   <code>true</code> if this is a built-in class.
    */
   public boolean isBuiltIn()
   {
      return builtin;
   }
   
   /**
    * Access the flag that denotes if this is a class definition.
    *
    * @return   <code>true</code> if this is a class definition.
    */
   public boolean isClass()
   {
      return otype == OOType.CLASS;
   }
   
   /**
    * Access the flag that denotes if this is an interface definition.
    *
    * @return   <code>true</code> if this is an interface definition.
    */
   public boolean isInterface()
   {
      return otype == OOType.INTERFACE;
   }
   
   /**
    * Access the flag that denotes if this is an enum definition.
    *
    * @return   <code>true</code> if this is an enum definition.
    */
   public boolean isEnum()
   {
      return otype == OOType.ENUM;
   }
   
   /**
    * Access the flag that denotes if this is a .NET class definition.
    *
    * @return   <code>true</code> if this is a .NET class.
    */
   public boolean isDotNet()
   {
      return dotnet;
   }

   /**
    * Access the flag that denotes if this is a Java class definition.
    *
    * @return   <code>true</code> if this is a Java class.
    */
   public boolean isJava()
   {
      return false;
   }

   /**
    * The method indicates whether this inctance represents a "mock" class definition
    * used to resolve symbols during class pre-scan mode in case the class file has not been
    * parsed.
    *
    * @return   <code>true</code> is this instance represents a "mock" class definition,
    *           <code>false</code> otherwise.
    */
   public boolean isMock()
   {
      return false;
   }
   
   /**
    * Get the next {@link #tempIdx} value.
    * 
    * @return   See above.
    */
   public int nextTempIdx()
   {
      return tempIdx++;
   }

   /**
    * Add the named method to this class. This is only called during parsing. 
    *
    * @param    name
    *           Method name.
    * @param    type
    *           Return type for the method.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the method is static.
    * @param    node
    *           The AST to be annotated or <code>null</code> if no node needs annotations.
    * @param    ret
    *           The AST for the method's return type.  This is used to copy OO annotations back to 
    *           the method node.
    */
   public void addMethod(String name, int type, int access, boolean isStatic, Aast node, Aast ret)
   {
      ParameterKey[] sig = SymbolResolver.calculateDefinitionSignature(node);
      
      // methods provide overloading so we can't use the simple member store mapping approach
      
      boolean isPreScan = SymbolResolver.isPreScan();
      
      int extent = 0;
      int mTempIdx = 0;
      
      synchronized (lock)
      {
         Map<SignatureKey, MemberData> defs = getMethodStore(name, isStatic);
      
         Map<String, Object> retType = new HashMap<>();
         
         if (ret != null)
         {
            Iterator<String> iter = ret.annotationKeys();
            
            while (iter.hasNext())
            {
               String akey = iter.next();
               if ("original-token".equals(akey))
               {
                  // this annotation has an AST as value, it just consumes memory if is kept.
                  continue;
               }
               retType.put(akey, ret.getAnnotation(akey));
            }
         }
         
         String     qname    = (String) retType.get("qualified");
         boolean    override = node != null && node.getImmediateChild(KW_OVERRIDE, null) != null;
         boolean    abstr    = node != null && node.getImmediateChild(KW_ABSTRACT, null) != null;
         boolean    register = !isPreScan || (builtin && !dotnet);
         MemberData prevMdat = defs.get(new SignatureKey(sig));
         
         extent = node == null ? 0 : SymbolResolver.readExtent(node);
         
         MemberData mdat = createMethod(defs, name, sig, access, isStatic, type, qname, 
                                        null, extent, tempIdx, override, abstr, retType, register);
         
         mdat.ast = node;
         
         if (prevMdat != null)
         {
            if (isPreScan)
            {
               // this can happen if we are referencing this class via a USING from an USING defined
               // in this file... in this case, inherit the tempidx, but show a warning.
               
               // TODO: these show up for constructors and many other cases; we can't fill up the logs with
               //       useless warnings, so we will disable for now
               // System.out.printf("\nERROR: duplicate method definition!\n");
               // System.out.printf("EXISTING %s\n", prevMdat.toString());
               // System.out.printf("DUPLICATE %s\n at %s", mdat.toString(), node.dumpTree());
            }
            
            // inherit the tempIdx computed at the pre-scan phase.
            mdat.tempIdx = prevMdat.tempIdx;
         }
         else
         {
            if (!isPreScan)
            {
               // there must always be a pre-scan definition...
               System.out.printf("\nERROR: no pre-scan method definition for \n");
               System.out.printf("%s\n", mdat.toString());
               
               return;
            }
            
            nextTempIdx();
         }
         
         mTempIdx = mdat.tempIdx;
      }
      
      // annotate the given node
      if (node != null)
      {
         node.putAnnotation("name", name);
         node.putAnnotation("return_type", Long.valueOf(type));
         node.putAnnotation("signature", SymbolResolver.calculateLegacySignature(name, sig)); 
         node.putAnnotation("access-mode", Long.valueOf(access));
         node.putAnnotation("static", Boolean.valueOf(isStatic));
         node.putAnnotation("oo-data-store", DataStoreType.METHOD.toString());
         node.putAnnotation("tempidx", Long.valueOf(mTempIdx));
         node.putAnnotation("tempidx-file", this.filename);
         
         if (extent != 0)
         {
            node.putAnnotation("extent", Long.valueOf(extent));
         }
         
         if (ret != null && ret.isAnnotation("qualified"))
         {
            String qname = (String) ret.getAnnotation("qualified");
            if (!(ret.isAnnotation("is-java") && (Boolean) ret.getAnnotation("is-java")))
            {
               qname = qname.toLowerCase();
            }
            node.putAnnotation("qualified", qname);
            
            // Because the "qualified" annotation is being inserted into ret during pre-scan,
            // we want to prevent these additional annotation requests, since they aren't
            // in the method's return node (ret).
            if (!isPreScan)
            {
               node.putAnnotation("source-file", (String) ret.getAnnotation("source-file"));
               node.putAnnotation("builtin-cls", (Boolean) ret.getAnnotation("builtin-cls"));
               node.putAnnotation("dotnet-cls", (Boolean) ret.getAnnotation("dotnet-cls"));
               node.putAnnotation("indirect-dotnet", (Boolean) ret.getAnnotation("indirect-dotnet"));
               node.putAnnotation("is-class", (Boolean) ret.getAnnotation("is-class"));
               node.putAnnotation("is-interface", (Boolean) ret.getAnnotation("is-interface"));
               node.putAnnotation("is-enum", (Boolean) ret.getAnnotation("is-enum"));
               node.putAnnotation("is-java", (Boolean) ret.getAnnotation("is-java"));
               
               if (ret.isAnnotation("generic-type-parameter"))
               {
                  String gtype = (String) ret.getAnnotation("generic-type-parameter");
               
                  node.putAnnotation("generic-type-parameter", gtype);
               }
               if (ret.isAnnotation("generic-type-is-primitive"))
               {
                  boolean prim = (boolean) ret.getAnnotation("generic-type-is-primitive");
                  
                  node.putAnnotation("generic-type-is-primitive", prim);
               }         
            }
         }
         
         // annotate the parameters
         SymbolResolver.processDefinitionSignature(node, (Aast ast, ParameterKey pk, int idx) -> 
         {
            ast.putAnnotation("jtype", pk.toJava());
         });
      }
   }
      
   /**
    * Find the named method without any knowledge of the signature. This method will search up
    * the parent hierarchy (recursively) if no match is found in the current class.
    * <p>
    * <b>If a positive value is returned, the caller can be assured that there is an accessible
    * method with that name BUT the actual type of the return value cannot be known until the
    * signature is known. This is due to the fact that the same method name can be present with
    * differing return values and only the parameter signature matching can differentiate the
    * exact method that will be called.</b>
    * <p>
    * <b>Do not call this from any location where the type must be known authoritatively.</b>
    *
    * @param    name
    *           Resource name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or
    *           <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    * @param    internal
    *           <code>true</code> if the lookup is internal to the current class definition.
    *
    * @return   The type found or <code>-1</code> if no match exists.
    */
   public int guessMethodType(String name, int access, boolean isStatic, boolean internal)
   {
      MemberData dat = guessMethod(name, access, isStatic, internal);
      
      return (dat == null) ? -1 : dat.type;
   }
   
   /**
    * Annotate a published class event, with the details of the parameter signature.
    * 
    * @param    evt
    *           The event node, annotated with its class definition.
    * @param    call
    *           The event publish node.
    * @param    isStatic
    *           <code>true</code> if the call is from a static context.
    */
   public void annotateObjectEvent(Aast evt, Aast call, boolean isStatic)
   {
      String evtName = evt.getText();
      int accessMode = (int) ((long) evt.getAnnotation("access-mode"));
      Variable var = lookupVariableWrapper(evtName, accessMode, isStatic);
      
      Aast evtSig = var.getDefinition().getImmediateChild(EVENT_SIGNATURE, null);
      
      // fixup parents
      ((AnnotatedAst)call).fixupParent(null);
      ParameterKey[] callsig = SymbolResolver.calculateCallSignature(call);
      ParameterKey[] defsig = SymbolResolver.calculateDefinitionSignature(evtSig);
      
      annotateCallSignature(call, callsig, defsig, null);
   }

   /**
    * Annotate the given method invocation reference with details for the specified method
    * in this class.  This is a no-operation if the method doesn't exist or if the node is
    * <code>null</code>.
    *
    * @param    mname
    *           Method name, {@code null} for a c'tor invocation.
    * @param    signature
    *           Array of type values for the parameter list, in left to right order.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the method is static.
    * @param    isSuper
    *           <code>true</code> if the rvalue refnode is the SUPER keyword.
    * @param    internal
    *           <code>true</code> if the lookup is internal to the current class definition.
    * @param    node
    *           The AST to be annotated.
    */
   public void annotateMethodCall(String         mname,
                                  ParameterKey[] signature,
                                  int            access,
                                  boolean        isStatic,
                                  boolean        isSuper,
                                  boolean        internal,
                                  Aast           node)
   {
      // manufacture a c'tor name for the lookup, if needed
      if (mname == null)
      {
         int idx = name.lastIndexOf('.');
         
         mname = ((idx < 0) ? name : name.substring(idx + 1)).toLowerCase();
      }
      
      if (node != null)
      {
         // go ahead and annotate the chp_wrapper right now too
         MethodSearchResult found = lookupMethodWorker(mname, signature, access, isStatic, internal, node);
         MemberData         mdat    = (found == null) ?  null : (MemberData) found.data;
         
         if (mdat != null && (isSuper || !(found != null && found.isDynamic())))
         {
            // method chain calls starting with SUPER keyword can not be morphed to dynamic invoke, as the
            // super calls require JVM to handle them.
            
            // the type may have been set incorrectly based on guessing, since the parameter
            // signature was not yet available; force it to the correct value here
            if (node.getType() != FUNC_CLASS && node.getType() != KW_THIS_OBJ && node.getType() != KW_SUPER)
            {
               node.setType(mdat.type);
            }
            
            // the access mode won't necessarily be the same as that passed in
            node.putAnnotation("access-mode", Long.valueOf(mdat.access));
            node.putAnnotation("static", Boolean.valueOf(mdat.isStatic));
            if (mdat.modes != null && !mdat.modes.isEmpty())
            {
               node.putAnnotation("param_modes", mdat.modes);
            }
            
            node.putAnnotation("tempidx", Long.valueOf(mdat.tempIdx));
            node.putAnnotation("tempidx-file", mdat.container.filename);
            
            node.putAnnotation("found-in-cls", mdat.container.name);
            if (mdat.container.builtin)
            {
               node.putAnnotation("builtin-cls", true);
            }
            if (mdat.container.dotnet)
            {
               node.putAnnotation("dotnet-cls", true);
            }
            node.putAnnotation("found-in-source-file", mdat.container.filename);
            
            if (mdat.extent != 0)
            {
               node.putAnnotation("extent", Long.valueOf(mdat.extent));
            }

            if (mdat.container.builtin && !mdat.container.dotnet)
            {
               String legacySignature = SymbolResolver.calculateLegacySignature(mdat.name, mdat.signature);
               String javaname = mdat.container.getConvertedMethodName(legacySignature);
               if (javaname != null)
               {
                  node.putAnnotation("javaname", javaname);
               }
            }
            else if (mdat.container.isEnum() && mdat.javaname != null)
            {
               node.putAnnotation("javaname", mdat.javaname);
            }
            
            // we cannot annotate the javaname at this point because we are currently in the non-pre-scan 
            // parse of a file and:
            // - a method call can reference a method definition that appears later in the file so the
            //   parsing must rely upon the pre-scan method def instead of the regular parse method def
            //   where the javaname is calculated
            // - can be a reference to a constructor which we currently don't calculate java names
            //
            // if (mdat.javaname != null)
            // {
            //    node.putAnnotation("javaname", mdat.javaname);
            // }
            // else
            // {
            //    // constructors don't have a javaname set
            //    if (!getSimpleName().equalsIgnoreCase(mname))
            //    {
            //       System.out.printf("WARNING: Missing javaname in %s\n\nfound %s\nfor location %s\n",
            //                         getName(),
            //                         mdat.toString(),
            //                         node.dumpTree(true));
            //    }
            // }
            
            annotateCallSignature(node, signature, mdat.signature, found.getOverrides());
            
            String legacySig = SymbolResolver.calculateLegacySignature(mdat.name, mdat.signature);
            node.putAnnotation("signature", legacySig);
            
            if (isBuiltIn())
            {
               // for builtin method calls, save the typelist too
               for (int i = 0; i < mdat.signature.length; i++)
               {
                  String type = mdat.signature[i].type;
                  int idx = type.indexOf('[');
                  if (idx >= 0)
                  {
                     type = type.substring(0, idx);
                  }
                  node.putAnnotation("typelist", type, i);
               }
            }
            
            if (mdat.type == OO_METH_CLASS)
            {
               if (mdat.qname == null)
               {
                  String err =
                     String.format("Missing class name for %s!", node.getDescriptiveTokenText());
                  throw new RuntimeException(err, new NoViableAltException((AnnotatedAst) node));
               }
               else
               {
                  node.putAnnotation("qualified", mdat.qname.toLowerCase());
                  
                  if (mdat.retType != null && mdat.retType.containsKey("generic-type-parameter"))
                  {
                     String gtype = (String) mdat.retType.get("generic-type-parameter");
                  
                     node.putAnnotation("generic-type-parameter", gtype);
                  }
                  if (mdat.retType != null && mdat.retType.containsKey("generic-type-is-primitive"))
                  {
                     boolean prim = (boolean) mdat.retType.get("generic-type-is-primitive");
                     
                     node.putAnnotation("generic-type-is-primitive", prim);
                  }         
               }
            }
         }
         else
         {
            // dynamic cases will have a non-null result; this may be for table-handle parms, dataset-handle
            // parms or POLY parms
            if (found != null && found.isDynamic())
            {
               node.putAnnotation("dynamic", true);
               if (mdat != null && mdat.isStatic)
               {
                  // we need to hard-code both the return type and the static class used as target, for these
                  // dynamic cases.
                  node.putAnnotation("dynamic-classname", mdat.container.name);
               }
               else if (isStatic)
               {
                  node.putAnnotation("dynamic-classname", this.name);
               }

               // constructors always return the expected type, so do not annotate them with 'dynamic-poly'
               boolean isCtor = node.getType() == ProgressParserTokenTypes.FUNC_CLASS && 
                                ((Long) node.getAnnotation("oldtype")) == ProgressParserTokenTypes.KW_NEW;
               if (found.isPolyArgs() && !isCtor)
               {
                  node.putAnnotation("dynamic-poly", true);
                  
                  for (int i = 0; i < mdat.signature.length; i++)
                  {
                     ParameterKey pkey = mdat.signature[i];
                     String ptype = pkey.toJava();
                     
                     node.putAnnotation("dynamic-poly-sig", ptype, -1);
                  }

                  if (mdat.type == OO_METH_CLASS)
                  {
                     if (mdat.qname == null)
                     {
                        String err =
                           String.format("Missing class name for %s!", node.getDescriptiveTokenText());
                        throw new RuntimeException(err, new NoViableAltException((AnnotatedAst) node));
                     }
                     else
                     {
                        node.putAnnotation("qualified", mdat.qname.toLowerCase());

                        if (mdat.retType != null && mdat.retType.containsKey("generic-type-parameter"))
                        {
                           String gtype = (String) mdat.retType.get("generic-type-parameter");
                        
                           node.putAnnotation("generic-type-parameter", gtype);
                        }
                        if (mdat.retType != null && mdat.retType.containsKey("generic-type-is-primitive"))
                        {
                           boolean prim = (boolean) mdat.retType.get("generic-type-is-primitive");
                           
                           node.putAnnotation("generic-type-is-primitive", prim);
                        }         
                     }
                  }
               }
            }
            else
            {
               // lookup failure, should not happen with syntactically correct code
               System.out.printf("\nannotateMethodCall FAILURE: %s from class %s, access %s, " +
                                 "static %b, at %s\n   PARAMETERS: %d\n",
                                 mname,
                                 getName(),
                                 ProgressParser.lookupTokenName(access),
                                 isStatic,
                                 node.dumpTree(),
                                 signature.length);
               for (int i = 0; i < signature.length; i++)
               {
                  System.out.printf("      %s\n", signature[i]);
               }
            }
         }
      }
   }
      
   /**
    * Add a data member (variable, property, enum or event) to this class.
    *
    * @param    type
    *           The token type of the member being added. This will be DEFINE_VARIABLE, DEFINE_PROPERTY,
    *           DEFINE_EVENT or DEFINE_ENUM.
    * @param    name
    *           Variable or property name.
    * @param    var
    *           Variable instance.
    * @param    node
    *           The definition node for this member.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the variable/property is static.
    * @param    qname
    *           Fully qualified class name where the resource represents an
    *           object instance, otherwise <code>null</code>.
    * @param    provisional
    *           {@code true} if this definition has not been finalized.
    */
   public void addVariable(int      type,
                           String   name,
                           Variable var,
                           Aast     node,
                           int      access,
                           boolean  isStatic,
                           String   qname,
                           boolean  provisional)
   {
      MemberData mdat = addWorker(DataStoreType.VAR,
                                  name,
                                  null,
                                  var,
                                  var.getTokenType(),
                                  access,
                                  isStatic,
                                  qname,
                                  -1);
      
      mdat.setProvisional(provisional);
      
      if (type == DEFINE_ENUM)
      {
         String ename = name.toLowerCase();
         
         if (javaEnumNames.containsKey(ename))
         {
            mdat.javaname = javaEnumNames.get(ename);
         }
      }
      
      if (mdat.javaname == null)
      {
         mdat.javaname = nconvert.convert(name, MatchPhraseConstants.TYPE_VARIABLE);
      }

      Consumer<ClassDefinition> consumer = (ClassDefinition cls) ->
      {
         resolvePossibleVariableConflicts(mdat, cls.istores);
         resolvePossibleVariableConflicts(mdat, cls.sstores);
      };
      consumer.accept(this);
      processParentGraph(consumer);

      if (node != null)
      {
         node.putAnnotation("oo-data-store", DataStoreType.VAR.toString());
         node.putAnnotation("javaname", mdat.javaname);
         if (mdat.suffix > 1)
         {
            node.putAnnotation("definitive-javaname", mdat.javaname);
         }
      }
   }

   /**
    * A method that resolves naming conflicts for properties by appending a suffix to the javaname.
    *
    * @param   mdat
    *          Container to track data associated with the current class member.
    * @param   stores
    *          Data stores.
    */
   private void resolvePossibleVariableConflicts(MemberData mdat,
                                                 Map<DataStoreType, Map<String, MemberData>> stores)
   {
      if (stores == null)
      {
         return;
      }
      for(DataStoreType dataStoreType: stores.keySet())
      {
         Map<String, MemberData> vars = stores.get(dataStoreType);
         if (vars == null)
         {
            return;
         }
         for (MemberData attribute : vars.values())
         {
            // if the current javaname collides with any other javaname or
            // a name of an attribute that is not suffixed, suffix the current javaname.
            if (attribute != null                                                          &&
                ((attribute.name.equals(mdat.javaname) && attribute.suffix == 1) ||
                 (attribute.javaname != null && attribute.javaname.equals(mdat.javaname))) &&
                !(attribute.name.equals(mdat.name) && dataStoreType == DataStoreType.VAR))
            {
               if (attribute.suffix > 1)
               {
                  mdat.javaname = mdat.javaname.substring(0, mdat.javaname.length() - 2);
               }
               mdat.javaname = mdat.javaname.concat("_" + attribute.suffix);
               mdat.suffix++;
               return;
            }
         }
      }
   }
   
   /**
    * Obtain the token type of the data member (variable) or property given
    * the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Variable or property name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    *
    * @return   Return token type or -1 if no such variable exists. 
    */
   public int lookupVariable(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.VAR, name, access, isStatic);
      return (mdat == null) ? -1 : mdat.type;
   }
   
   /**
    * Obtain the Java name of the data member the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent (if one exists) will be checked.
    * This means a recursive call can occur here.
    *
    * @param    name
    *           Variable or property name.
    * @param    access
    *           Access mode ({@code KW_PUBLIC}, {@code KW_PROTECTD} or {@code KW_PRIVATE}).
    * @param    isStatic    
    *           <code>true</code> if this is a static reference.
    *
    * @return   The Java name of the data member or {@code null} if no such variable exists. 
    */
   public String lookupVariableJavaName(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.VAR, name, access, isStatic);
      return (mdat == null) ? null : mdat.javaname;
   }
   
   /**
    * Obtain the extent of the data member the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent (if one exists) will be checked.
    * This means a recursive call can occur here.
    *
    * @param    name
    *           Variable or property name.
    * @param    access
    *           Access mode ({@code KW_PUBLIC}, {@code KW_PROTECTD} or {@code KW_PRIVATE}).
    * @param    isStatic    
    *           <code>true</code> if this is a static reference.
    *
    * @return   The extent of the data member or 0 if the variable is not an extent var or if no such
    *           variable exists. 
    */
   public long lookupVariableExtent(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.VAR, name, access, isStatic);
      return (mdat == null || mdat.var.getExtent() == 0) ? 0L : (long) mdat.var.getExtent();
   }
   
   
   /**
    * Obtain the fully qualified class name of the object instance 
    * represented by the data member (variable) or property given
    * the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Variable or property name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic    
    *           <code>true</code> if this is a static reference.
    *
    * @return   Fully qualified class name of the the object instance
    *           represented by this variable or data member, or 
    *           <code>null</code> if no such variable exists. 
    */
   public String lookupVariableClassName(String  name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.VAR, name, access, isStatic);
      return (mdat == null) ? null : mdat.qname;
   }
   
   /**
    * Obtain the <code>Variable</code> wrapper for the data member (variable) or property given
    * the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent (if one exists) will
    * be checked. This means a recursive call can occur here.
    *
    * @param    name
    *           Variable or property name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic    
    *           <code>true</code> if this is a static reference.
    *
    * @return   The associated wrapper or <code>null</code> if no such variable exists. 
    */
   public Variable lookupVariableWrapper(String  name, int access, boolean isStatic)
   {
      return lookupVariableWrapper(name, access, isStatic, false);
   }

   /**
    * Obtain the <code>Variable</code> wrapper for the data member (variable) or property given
    * the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent (if one exists) will
    * be checked. This means a recursive call can occur here.
    *
    * @param    name
    *           Variable or property name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic    
    *           <code>true</code> if this is a static reference.
    * @param    chainedReference
    *           Flag indicating that the reference being looked is from a ":" chain.
    *
    * @return   The associated wrapper or <code>null</code> if no such variable exists. 
    */
   public Variable lookupVariableWrapper(String  name, int access, boolean isStatic, boolean chainedReference)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.VAR, name, access, isStatic, chainedReference);
      return (mdat == null) ? null : mdat.var;
   }
   
   /**
    * Cleanup any object resources which should not remain in memory.  The only resources
    * that will be used after this point are ones which can be looked up by other classes.
    */
   public void cleanupObjectResources()
   {
      synchronized (lock)
      {
         if (schemaDict != null)
         {
            ArrayList<String> saveList = new ArrayList<String>();
            
            defineExceptions(saveList, false);
            defineExceptions(saveList, true);
            
            if (saveList.isEmpty())
            {
               // nothing to save, just release the reference.
               schemaDict = null;
            }
            else
            {
               String[] save = saveList.toArray(new String[0]);
               schemaDict.removeAllExcept(save);
            }
         }
      }
   }
   
   /**
    * Obtain the backing schema dictionary, if it exists.
    *
    * @return   The schema dictionary or <code>null</code> if it was never set.
    */
   public SchemaDictionary getSchemaDict()
   {
      synchronized (lock)
      {
         return schemaDict;
      }
   }
   
   /**
    * Add the named temp-table or buffer to this class.
    *
    * @param    node
    *           The definition AST.
    * @param    name
    *           Temp-table or buffer name.
    * @param    type
    *           The integer token type representing the type of table. 
    * @param    access
    *           Access mode (<code>KW_PROTECTD</code> or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the method is static.
    * @param    schemaDict
    *           The schema dictionary that manages the temp-tables and buffers for this
    *           class.
    * @param    inMethod
    *           Flag indicating we are in a method definition.
    */
   public void addTable(Aast             node,
                        String           name,
                        int              type,
                        int              access,
                        boolean          isStatic,
                        SchemaDictionary schemaDict,
                        boolean          inMethod)
   {
      synchronized (lock)
      {
         if (this.schemaDict == null)
         {
            this.schemaDict = schemaDict;
         }
      }
      
      MemberData mdat = addWorker(DataStoreType.TABLE, name, null, null, type, access, isStatic, null, -1);
      // temp-tables are never provisional
      mdat.setProvisional(type == BUFFER && (SymbolResolver.isPreScan() || inMethod));
      if (!mdat.provisional)
      {
         mdat.ast = node;
         
         if (node != null)
         {
            node.putAnnotation("access-mode", access == -1 ? (long) KW_PRIVATE : (long) access);
            node.putAnnotation("oo-data-store", DataStoreType.TABLE.toString());

            if (isStatic)
            {
               node.putAnnotation("static", isStatic);
            }
         }
      }
   }
      
   /**
    * Obtain the token type of the temp-table or buffer given the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Temp-table or buffer name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    *
    * @return   Return token type or -1 if no such variable exists. 
    */
   public int lookupTable(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.TABLE, name, access, isStatic);
      return (mdat == null) ? -1 : mdat.type;
   }
   
   /**
    * Add the named query to this class.
    *
    * @param    name
    *           Query name.
    * @param    node
    *           The AST to be annotated or <code>null</code> if no node needs annotations.
    * @param    type
    *           The integer token type representing the type of table. 
    * @param    access
    *           Access mode (<code>KW_PROTECTD</code> or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the method is static.
    */
   public void addQuery(String  name, 
                        Aast    node, 
                        int     type, 
                        int     access, 
                        boolean isStatic,
                        boolean inMethod)
   {
      MemberData mdat = addWorker(DataStoreType.QRY, name, null, null, type, access, isStatic, null, -1);
      mdat.setProvisional(SymbolResolver.isPreScan() || inMethod);
      
      if (!mdat.provisional)
      {
         mdat.ast = node;
         
         if (node != null)
         {
            node.putAnnotation("access-mode", access == -1 ? (long) KW_PRIVATE : (long) access);
            node.putAnnotation("oo-data-store", DataStoreType.QRY.toString());

            if (isStatic)
            {
               node.putAnnotation("static", isStatic);
            }
         }
      }
   }
      
   /**
    * Obtain the token type of the query member given the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Query name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    *
    * @return   Return token type or -1 if no such variable exists. 
    */
   public int lookupQuery(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.QRY, name, access, isStatic);
      return (mdat == null) ? -1 : mdat.type;
   }
   
   /**
    * Add the named dataset to this class.
    *
    * @param    var
    *           The associated variable for this data-set.
    * @param    node
    *           The definition AST.
    * @param    name
    *           Dataset name.
    * @param    type
    *           The integer token type representing the type of table. 
    * @param    access
    *           Access mode (<code>KW_PROTECTD</code> or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the method is static.
    * @param    inMethod
    *           Flag indicating we are in a method definition.
    */
   public void addDataSet(Variable var,
                          Aast node,
                          String name,
                          int type,
                          int access,
                          boolean isStatic,
                          boolean inMethod)
   {
      MemberData mdat = addWorker(DataStoreType.DATASET, name, null, var, type, access, isStatic, null, -1);
      // datasets can't be defined in a class method
      mdat.setProvisional(false);
      if (!mdat.provisional)
      {
         mdat.ast = node;
         
         if (node != null)
         {
            node.putAnnotation("access-mode", access == -1 ? (long) KW_PRIVATE : (long) access);
            node.putAnnotation("oo-data-store", DataStoreType.DATASET.toString());
            
            if (isStatic)
            {
               node.putAnnotation("static", isStatic);
            }
         }
      }
   }
      
   /**
    * Obtain the token type of the dataset member given the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Dataset name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    *
    * @return   Return token type or -1 if no such dataset exists. 
    */
   public int lookupDataSet(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.DATASET, name, access, isStatic);
      return (mdat == null) ? -1 : mdat.type;
   }
   
   /**
    * Obtain the token type of the dataset member given the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Dataset name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    *
    * @return   Return token type or -1 if no such dataset exists. 
    */
   public Variable lookupDataSetWrapper(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.DATASET, name, access, isStatic);
      return (mdat == null) ? null : mdat.var;
   }

   /**
    * Add the named data source to this class.
    *
    * @param    var
    *           The associated variable for this data-set.
    * @param    node
    *           The definition AST.
    * @param    name
    *           Data source name.
    * @param    type
    *           The integer token type representing the type of table. 
    * @param    access
    *           Access mode (<code>KW_PROTECTD</code> or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the method is static.
    * @param    inMethod
    *           Flag indicating we are in a method definition.
    */
   public void addDataSource(Variable var,
                             Aast node,
                             String name,
                             int type,
                             int access,
                             boolean isStatic,
                             boolean inMethod)
   {
      MemberData mdat = addWorker(DataStoreType.DATASRC, name, null, var, type, access, isStatic, null, -1);
      mdat.setProvisional(SymbolResolver.isPreScan() || inMethod);
      if (!mdat.provisional)
      {
         mdat.ast = node;
         
         if (node != null)
         {
            node.putAnnotation("access-mode", access == -1 ? (long) KW_PRIVATE : (long) access);
            node.putAnnotation("oo-data-store", DataStoreType.DATASRC.toString());

            if (isStatic)
            {
               node.putAnnotation("static", isStatic);
            }
         }
      }
   }
      
   /**
    * Obtain the token type of the data source member given the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Data source name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    *
    * @return   Return token type or -1 if no such data source exists. 
    */
   public int lookupDataSource(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.DATASRC, name, access, isStatic);
      return (mdat == null) ? -1 : mdat.type;
   }

   /**
    * Obtain the token type of the data source member given the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Data source name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    *
    * @return   Return token type or -1 if no such data source exists. 
    */
   public Variable lookupDataSourceWrapper(String name, int access, boolean isStatic)
   {
      MemberData mdat = lookupHierarchy(DataStoreType.DATASRC, name, access, isStatic);
      return (mdat == null) ? null : mdat.var;
   }

   /**
    * Reports if the given data member is a static member of the given class.
    *
    * @param    name
    *           Member name to lookup.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    *
    * @return   <code>true</code> if the member is static.  <code>false</code> otherwise
    *           including the case where the member does not exist at all.
    */
   public boolean isStaticDataMember(String name, int access)
   {
      return lookupHierarchy(DataStoreType.VAR, name, access, true) != null;
   }

   /**
    * Get all defined methods in this class.
    * 
    * @return   All defined method names.
    */
   public Set<String> getDefinedMethods()
   {
      Set<String> res = new HashSet<>();
      
      if (imethods != null)
      {
         imethods.values().forEach(mp -> mp.values().forEach(md -> res.add(md.name)));
      }
      if (smethods != null)
      {
         smethods.values().forEach(mp -> mp.values().forEach(md -> res.add(md.name)));
      }
      
      return res;
   }
   
   /**
    * Recursively process the parent hierarchy to test if the given parent matches some
    * condition.  This searches the list of parents, checking UP before SIDEWAYS.
    *
    * @param    check
    *           Delegated code to check the condition.
    *
    * @return   {@code true} if the condition is matched anywhere in the parent graph.
    */
   public boolean testParentGraph(Predicate<ClassDefinition> check)
   {
      boolean result = false;
      
      if (parents != null)
      {
         for (ClassDefinition parent : parents)
         {
            result = check.test(parent);
            
            if (result)
            {
               break;
            }
            else
            {
               // recurse
               result = parent.testParentGraph(check);
               
               if (result)
               {
                  break;
               }
            }
         }
      }
      
      return result;
   }
   
   /**
    * Recursively process the parent hierarchy to calculate a value.  This searches the list of
    * parents, checking UP before SIDEWAYS.
    *
    * @param    func
    *           Delegated code to calculate the value.
    * @param    continu
    *           A condition which if it returns <code>true</code>, then the next parent should be tried.
    *           On <code>false</code>, the current result is returned.
    * @param    defVal
    *           Default value to return if no parents exist.
    * @param    inherit
    *           {@code true} to search the inheritance hierarchy for a match.
    * @param    ifaces
    *           {@code true} to search interfaces (if any exist) in addition to direct parents.
    *
    * @return   The calculated value or {@code null} if no value is calculated.
    */
   public <R> R calcFromParentGraph(java.util.function.Function<ClassDefinition, R> func,
                                    Predicate<R>                                    continu,
                                    R                                               defVal,
                                    boolean                                         inherit,
                                    boolean                                         ifaces)
   {
      Set<String> processed = new HashSet<>();
      
      return calcFromParentGraphWorker(func, continu, defVal, processed, inherit, ifaces);
   }
   
   /**
    * Recursively process the parent hierarchy to execute a consumer.  This searches the list of
    * parents, checking UP before SIDEWAYS.
    *
    * @param    consumer
    *           Delegated code to process the value.
    */
   public void processParentGraph(Consumer<ClassDefinition> consumer)
   {
      processParentGraph(consumer, true);
   }
   
   /**
    * Recursively process the parent hierarchy to execute a consumer.  This searches the list of
    * parents, checking UP before SIDEWAYS.
    *
    * @param    consumer
    *           Delegated code to process the value.
    * @param    descent
    *           Flag indicating if the processing is done on descent (from nearest parent to object) or ascent
    *           (from object to nearest parent).
    */
   public void processParentGraph(Consumer<ClassDefinition> consumer, boolean descent)
   {
      if (parents != null)
      {
         for (ClassDefinition parent : parents)
         {
            if (descent)
            {
               consumer.accept(parent);
            }
            parent.processParentGraph(consumer, descent);
            if (!descent)
            {
               consumer.accept(parent);
            }
         }
      }
   }
   
   /**
    * Check if any parent class has the specified accessor.  This is required to properly emit 
    * <code>Override</code> annotations only for getters/setters defined in the abstract property.
    * 
    * @param    ast
    *           The accessor AST.
    *           
    * @return   <code>true</code> if a parent defines this getter/setter.
    */
   public boolean hasParentPropertyAccessor(Aast ast)
   {
      if (parents == null)
      {
         return false;
      }
      
      int type = ast.getType();
      if (!(type == KW_GET || type == KW_SET))
      {
         return false;
      }
      
      String property = (String) ast.getParent().getAnnotation("name");
      boolean getter = (type == KW_GET);
      
      java.util.function.Function<ClassDefinition, Boolean> test = (ClassDefinition parent) ->
      {
         Variable var = parent.lookupVariableWrapper(property, KW_PROTECTD, false);
         
         return var != null && (getter ? var.isWithGetter() : var.isWithSetter());
      };
      
      Boolean res = calcFromParentGraph(test, (b) -> !b, false, true, true);
      return res != null && res;
   }

   /**
    * Recursively process the parent hierarchy to calculate a value.  This searches the list of
    * parents, checking UP before SIDEWAYS.
    *
    * @param    func
    *           Delegated code to calculate the value.
    * @param    continu
    *           A condition which if it returns <code>true</code>, then the next parent should be tried.
    *           On <code>false</code>, the current result is returned.
    * @param    defVal
    *           Default value to return if no parents exist.
    * @param    inherit
    *           {@code true} to search the inheritance hierarchy for a match.
    * @param    ifaces
    *           {@code true} to search interfaces (if any exist) in addition to direct parents.
    *
    * @return   The calculated value or {@code null} if no value is calculated.
    */
   private <R> R calcFromParentGraphWorker(java.util.function.Function<ClassDefinition, R> func,
                                          Predicate<R>                                    continu,
                                          R                                               defVal,
                                          Set<String>                                     processed,
                                          boolean                                         inherit,
                                          boolean                                         ifaces)
   {
      R result = defVal;
      
      int start = 0;
      int num   = ((inherit && parents != null) ? parents.length : 0) +
                  ((ifaces && interfaces != null) ? interfaces.length : 0);
      
      ClassDefinition[] list = new ClassDefinition[num];
      
      if (inherit && parents != null)
      {
         System.arraycopy(parents, 0, list, start, parents.length);
         start = parents.length;
      }
      
      if (ifaces && interfaces != null)
      {
         System.arraycopy(interfaces, 0, list, start, interfaces.length);
      }
      
      for (ClassDefinition parent : list)
      {
         // avoid duplicate processing
         if (processed.contains(parent.getName()))
         {
            continue;
         }
         
         processed.add(parent.getName());
         
         result = func.apply(parent);

         if (!continu.test(result))
         {
            break;
         }
         else
         {
            // recurse
            result = parent.calcFromParentGraphWorker(func, continu, defVal, processed, inherit, ifaces);
            
            if (!continu.test(result))
            {
               break;
            }
         }
      }
      
      return result;
   }
   
   /**
    * Obtain the static or instance member map or create (and return) it if it doesn't
    * already exist.
    *
    * @param    type
    *           The data store type to lookup.
    * @param    isStatic
    *           <code>true</code> if the static map is to be returned, <code>false</code> for
    *           the instance version.
    *
    * @return   The specified map. It will not be <code>null</code>.
    */
   private Map<String, MemberData> getMemberStore(DataStoreType type, boolean isStatic)
   {
      Map<String, MemberData> data = null;
      
      synchronized (lock)
      {
         if (isStatic && sstores == null)
         {
            sstores = new LinkedHashMap<DataStoreType, Map<String, MemberData>>();
         }
         if (!isStatic && istores == null)
         {
            istores = new LinkedHashMap<DataStoreType, Map<String, MemberData>>();
         }
         
         data = isStatic ? sstores.get(type) : istores.get(type);
         
         if (data == null)
         {
            data = new LinkedHashMap<String, MemberData>();
            
            if (isStatic)
            {
               sstores.put(type, data);
            }
            else
            {
               istores.put(type, data);
            }
         }
      }
      
      return data;
   }
   
   /**
    * Get the correct map of method signatures to member data instances.  This must only be called when the
    * lock instance is owned. 
    *
    * @param    name
    *           Method name.
    * @param    isStatic
    *           <code>true</code> if the method is static.
    *           
    * @return   The method map.
    */   
   private Map<SignatureKey, MemberData> getMethodStore(String name, boolean isStatic)
   {
      if (imethods == null)
      {
         imethods = new LinkedHashMap<>();
         smethods = new LinkedHashMap<>();
      }

      Map<String, Map<SignatureKey, MemberData>> store = isStatic ? smethods : imethods;
      String                                     key   = name.toLowerCase();
      Map<SignatureKey, MemberData>              defs  = store.get(key);
   
      if (defs == null)
      {
         defs = new LinkedHashMap<>();
         store.put(key, defs);
      }

      return defs;
   }
   
   /**
    * Obtain the member given the name and access mode. If the member does not exist in
    * this class definition the parent (if one exists) will be checked.
    *
    * @param    type
    *           Type of data store to check.
    * @param    name
    *           Variable/data member name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    *           
    * @return   Member data or <code>null</code> if no such variable or data
    *           member exists. 
    */
   private MemberData lookupHierarchy(DataStoreType type,
                                      String        name,
                                      int           access,
                                      boolean       isStatic)
   {
      return lookupHierarchy(type, name, access, isStatic, false);
   }
   
   /**
    * Obtain the member given the name and access mode. If the member does not exist in
    * this class definition the parent (if one exists) will be checked.
    *
    * @param    type
    *           Type of data store to check.
    * @param    name
    *           Variable/data member name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    * @param    chainedReference
    *           Flag indicating that the reference being looked is from a ":" chain.
    *           
    * @return   Member data or <code>null</code> if no such variable or data
    *           member exists. 
    */
   private MemberData lookupHierarchy(DataStoreType type,
                                      String        name,
                                      int           access,
                                      boolean       isStatic,
                                      boolean       chainedReference)
   {
      MemberData mdat = null;
      
      synchronized (lock)
      {
         mdat = lookupWorker(getMemberStore(type, isStatic), name, access);

         if (mdat == null && !isStatic)
         {
            // can't find an instance member, check for static
            mdat = lookupWorker(getMemberStore(type, true), name, access);
         }
         
         if (mdat != null && mdat.provisional)
         {
            if (chainedReference)
            {
               // if a match is done on a provisional reference, while in a chain, then there must be a 
               // definition in current class
               mdat.setProvisional(false);
            }
            else
            {
               // provisional members can't be found
               mdat = null;
            }
         }

         if (mdat == null && parents != null)
         {
            // change access mode since we aren't allowed to see private stuff
            // in parent
            if (access == KW_PRIVATE)
               access = KW_PROTECTD;
            
            for (ClassDefinition parent : parents)
            {
               // recurse up inheritence hierarchy
               mdat = parent.lookupHierarchy(type, name, access, isStatic);
               
               if (mdat != null)
                  break;
            }
         }
         
         // if only static members will match
         if (mdat != null && isStatic && !mdat.isStatic)
         {
            // clear our result
            mdat = null;
         }
         
         if (mdat == null && type == DataStoreType.VAR && !isStatic && interfaces != null)
         {
            // lookup in interfaces, too
            for (ClassDefinition ifaceDef : interfaces)
            {
               mdat = ifaceDef.lookupHierarchy(type, name, access, isStatic);
               if (mdat != null)
               {
                  break;
               }
            }
         }
      }
      
      return mdat;
   }
   
   /**
    * Process a method call to mark any "fuzzy" parameters with the wrapper type that must
    * be used for the actual call.
    * 
    * @param    call
    *           The OO_METH_* node whose parameters will be annotated.
    * @param    callSig
    *           The array of parameter types that is the method definition signature.
    * @param    defSig
    *           The array of parameter types that is the method definition signature.
    * @param    overrides
    *           Type overrides found by any fuzzy matching for this method.
    */
   private void annotateCallSignature(Aast           call,
                                      ParameterKey[] callSig,
                                      ParameterKey[] defSig,
                                      String[]       overrides)
   {
      int idx = 0;
      
      // iterate all parameters and extract their types
      Aast parm = (Aast) call.getFirstChild();
      if ((call.isAnnotation("oldtype") && (long) call.getAnnotation("oldtype") == KW_NEW) || 
          call.getType() == KW_THIS_OBJ                                                    ||
          call.getType() == KW_SUPER)
      {
         parm = (Aast) call.getImmediateChild(LPARENS, null);
         parm = (Aast) parm.getFirstChild();
      }

      while (parm != null)
      {
         int type = parm.getType();
         
         // avoid the optional mode or table parm modifiers
         if (type != KW_PUBLISH  &&
             type != CLASS_EVENT &&
             type != KW_INPUT    &&
             type != KW_IN_OUT   &&
             type != KW_OUTPUT   &&
             type != KW_APPEND   && 
             type != KW_BIND     &&
             type != KW_BY_REF   &&
             type != KW_BY_VALUE)
         {
            parm.putAnnotation("jtype", defSig[idx].toJava());
            
            parm.putAnnotation("parmtype", defSig[idx].mode.longValue());
            if (parm.getType() == PARAMETER)
            {
               Aast ref = parm.getChildAt(parm.getNumImmediateChildren() - 1);
               ref.putAnnotation("parmtype", defSig[idx].mode.longValue());
            }
            
            boolean outputMode = defSig[idx].mode != null && defSig[idx].mode.intValue() != KW_INPUT;

            String classname = defSig[idx].type;
            String callType = callSig[idx].type;
            String defType = defSig[idx].type;
            
            int idx1 = defType.indexOf("[");
            int idx2 = callType.indexOf("[");
            
            // remove the [] from the classname/callType
            if (idx1 > 0)
            {
               classname = defType.substring(0, idx1);
            }
            
            if (outputMode)
            {
               if (!callSig[idx].isExtent())
               {
                  // this is not required to be set for extent arguments 
                  parm.putAnnotation("assign_back", true);
                  if (defSig[idx].mode.intValue() == KW_IN_OUT)
                  {
                     parm.putAnnotation("needs_value", true);
                  }
                  
                  if (parm.getType() == PARAMETER)
                  {
                     // copy these to the reference, too
                     Aast ref = parm.getChildAt(parm.getNumImmediateChildren() - 1);
                     ref.putAnnotation("assign_back", true);
                     if (defSig[idx].mode.intValue() == KW_IN_OUT)
                     {
                        ref.putAnnotation("needs_value", true);
                     }
                  }
               }
            }
            
            if (!defType.equals(callType))
            {
               // force everything for now
               boolean force = true;

               if (classname.startsWith("BUFFER ")       || 
                   classname.startsWith("TEMP-TABLE")    ||
                   classname.equals("TABLE-HANDLE")      ||
                   classname.startsWith("DATASET ")      ||
                   classname.equals("DATASET-HANDLE")    ||
                   (!"unknown".equals(callType) && 
                    (classname.startsWith("object<") ||
                     classname.startsWith("jobject<"))))
               {
                  force = false;
               }
               else if (isExtentMatch(defType, callType))
               {
                  // non-extents and extents are matched here

                  // both caller and def are extent
                  if (idx1 != -1 && idx2 != -1)
                  {
                     String prefix1 = defType.substring(0, idx1);
                     String prefix2 = callType.substring(0, idx2);
                     
                     // compare data types
                     if (prefix1.equals(prefix2))
                     {
                        // extent differences alone don't need wrapping
                        force = false;
                     }
                     else
                     {
                        // TODO: conversion needs to be enhanced to support wrapping array parms
                        System.out.printf("WARNING: FUZZY TYPE EXTENT detected at parm %d " +
                                          "(def = %s, call = %s), node = %s\n",
                                          idx,
                                          defSig[idx],
                                          callSig[idx],
                                          call.dumpTree());
                     }
                  }
                  // the caller is extent or the def is extent but not both
                  else if (idx1 != -1 || idx2 != -1)
                  {
                     // TODO: conversion needs to be enhanced to support wrapping array parms
                     System.out.printf("WARNING: FUZZY TYPE EXTENT detected at parm %d " +
                                       "(def = %s, call = %s), node = %s\n",
                                       idx,
                                       defSig[idx],
                                       callSig[idx],
                                       call.dumpTree());
                  }
                  // neither caller nor def are extent
                  else if (parm.getType() == FUNC_CLASS    || 
                           parm.getType() == VAR_CLASS     ||
                           parm.getType() == FIELD_CLASS   ||
                           parm.getType() == FUNC_CLASS    ||
                           parm.getType() == ATTR_CLASS    ||
                           parm.getType() == METH_CLASS    ||
                           parm.getType() == OO_METH_CLASS)
                  {
                     force = false;
                  }
               }

               // do not wrap the last parameter for Progress.Lang.Class:setParameter
               if (!(parm.getIndexPos() == 3                                     && 
                     parm.getParent().getType() == OO_METH_LOGICAL               && 
                     parm.getParent().getText().equalsIgnoreCase("setParameter") &&
                     parm.getParent().getAnnotation("found-in-cls")
                                     .toString()
                                     .equalsIgnoreCase("Progress.Lang.ParameterList")))
               {
                  if (force)
                  {
                     if (callType.indexOf('[') < 0)
                     {
                        if (parm.getType() == UNKNOWN_VAL || 
                            (parm.getType() == PARAMETER && parm.getFirstChild().getType() == UNKNOWN_VAL))
                        {
                           if (parm.getType() == PARAMETER)
                           {
                              parm.putAnnotation("wrap_parameter", true);
                           }
                        }
                        else if (!classname.equals("BaseDataType") &&
                                 !classname.equals("Object"))
                        {
                           parm.putAnnotation("wrap_parameter", true);
                        }
                     }

                     if (idx2 > 0)
                     {
                        callType = callType.substring(0, idx2);
                     }                     
                     
                     Aast ref = parm;
                     if (classname.indexOf('<') > 0)
                     {
                        classname = classname.substring(0, classname.indexOf('<'));
                        
                        if (ref.downPath(UNKNOWN_VAL) || ref.downPath(EXPRESSION, UNKNOWN_VAL))
                        {
                           ref.removeAnnotation("wrap_parameter");
                           ref = (Aast) ref.getFirstChild();
                        }
                     }
                     
                     ref.putAnnotation("classname", classname);
                     ref.putAnnotation("calltype", callType);
                  }
               }
            }
            
            idx++;
         }
         
         parm = (Aast) parm.getNextSibling();
      }
   }
      
   /**
    * Find the named method without any knowledge of the signature. This method will search up
    * the parent hierarchy (recursively) if no match is found in the current class.
    * <p>
    * <b>If a non-null value is returned, the caller can be assured that there is an accessible
    * method with that name BUT the actual type/details of the method cannot be known until the
    * signature is known. This is due to the fact that the same method name can be present with
    * differing return values/parameter signature and only the parameter signature matching can
    * differentiate the exact method that will be called.</b>
    * <p>
    * <b>Do not call this from any location where the method details must be known
    * authoritatively.</b>
    *
    * @param    name
    *           Resource name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or
    *           <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    * @param    internal
    *           <code>true</code> if the lookup is internal to the current class definition.
    *
    * @return   The method found or <code>null</code> if no match exists.
    */
   private MemberData guessMethod(String name, int access, boolean isStatic, boolean internal)
   {
      MemberData mdat = null;

      synchronized (lock)
      {
         Map<String, Map<SignatureKey, MemberData>> map = new LinkedHashMap<>();
         if (isStatic)
         {
            if (smethods != null)
            {
               map.putAll(smethods);
            }
            else
            {
               map = null;
            }
         }
         else
         {
            // here we can access both static and instance methods
            if (smethods == null && imethods == null)
            {
               map = null;
            }
            
            if (smethods != null)
            {
               map.putAll(smethods);
            }

            if (imethods != null)
            {
               map.putAll(imethods);
            }
         }
         
         if (map != null)
         {
            Map<SignatureKey, MemberData> defs = map.get(name.toLowerCase());
            
            if (defs != null)
            {
               Iterator<MemberData> iter = defs.values().iterator();
               
               while (iter.hasNext())
               {
                  MemberData dat = checkAccessRights(iter.next(), access);
                  
                  if (dat != null)
                  {
                     // if we are here, we have an access rights match, but we must ensure that
                     // there is a static/instance match
                     if ((!isStatic && (!dat.isStatic || internal)) ||
                         (isStatic && dat.isStatic))
                     {
                        // done!
                        return dat;
                     }
                  }
               }
            }
         }
         
         // there was no match in the current class, look up the parent hierarchy
         if (parents != null)
         {
            // change access mode since we aren't allowed to see private stuff in parent
            int _access = (access == KW_PRIVATE) ? KW_PROTECTD : access;
            
            for (ClassDefinition parent : parents)
            {
               // recurse up inheritence hierarchy
               mdat = parent.guessMethod(name, _access, isStatic, internal);
               
               if (mdat != null)
                  break;
            }
         }
      }
      
      if (mdat == null && !isStatic && internal)
      {
         // fallback to static method check, when looking an instance method
         mdat = guessMethod(name, access, true, internal);
      }
      
      // no match anywhere
      return mdat;
   }
   
   /**
    * Obtain the method's member given the name and access mode. If the member does not exist in
    * this class definition the parent (if one exists) will be checked.
    *
    * @param    name
    *           Method name.
    * @param    sig
    *           Array describing the signature of the method call. 
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    * @param    internal
    *           <code>true</code> if the lookup is internal to the current class definition.
    * @param    node
    *           The AST referencing this method.
    *
    * @return   Member data or <code>null</code> if no such variable or data
    *           member exists. 
    */
   private MethodSearchResult lookupMethodWorker(String         name,
                                                 ParameterKey[] sig,
                                                 int            access,
                                                 boolean        isStatic,
                                                 boolean        internal,
                                                 Aast           node)
   {
      int type = node.getType();
      boolean constructor = type == KW_SUPER    || 
                            type == KW_THIS_OBJ || 
                            (type == FUNC_CLASS && (long) node.getAnnotation("oldtype") == KW_NEW);

      MethodSearchResult result = null;
      
      synchronized (lock)
      {
         result = exactMethodLookup(name, sig, access, isStatic, internal, constructor);

         if (result == null && !isStatic && internal)
         {
            // can't find an instance member, check for static
            result = exactMethodLookup(name, sig, access, true, internal, constructor);
         }
         
         if (result == null)
         {
            result = fuzzy.fuzzyMethodLookup(name, sig, access, isStatic, internal, node, constructor);
   
            if (result == null && !isStatic && internal)
            {
               // can't find an instance member, check for static
               result = fuzzy.fuzzyMethodLookup(name, sig, access, true, internal, node, constructor);
            }
         }
      }
      
      return result;
   }
   
   /**
    * Find the named method based on an exact signature match. This method searches all methods in the
    * parent hierarchy not just in the current class.  This search the hierarchy approach is needed so
    * that some special exceptions can be honored:
    * <p>
    * <ul>
    *    <li>If the caller does not specify a mode and there is an otherwise exact match, that method is
    *        selected.  Buffers have no mode processing so this case is ignored for them.</li>
    *    <li>A caller's table-handle or dataset-handle parameter which has an exact match is disallowed
    *        if there are any alternatives with a table/dataset at that parameter AND that parameter type
    *        is what determines that the signatures are different.</li>
    * </ul>
    *
    * @param    name
    *           Resource name.
    * @param    caller
    *           Method call signature.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or
    *           <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    * @param    internal
    *           <code>true</code> if the lookup is internal to the current class definition.
    * @param    constructor
    *           Flag indicating this must resolve a constructor.
    *
    * @return   Resource found or <code>null</code> if no match exists.
    */
   private MethodSearchResult exactMethodLookup(final String         name,
                                                final ParameterKey[] caller,
                                                final int            access,
                                                final boolean        isStatic,
                                                final boolean        internal,
                                                final boolean        constructor)
   {
      List<MatchMetrics> list = candidates(name, caller.length, access, isStatic, internal, false, null, 
                                           constructor);
      
      // quick out if there are no possible matches
      if (list.isEmpty())
      {
         return null;
      }

      final boolean[] ignore = new boolean[caller.length];
      
      Predicate<MatchMetrics> exactMatch = (mem) -> 
      {
         for (int i = 0; i < caller.length; i++)
         {
            if (!ignore[i] && !caller[i].equals(mem.data.getSignature(i)))
            {
               return false;
            }
         }
          
         return true;
      };
       
      List<MatchMetrics> exact = list.stream().filter(exactMatch).collect(Collectors.toList());

      Predicate<MatchMetrics> flexModeMatch = (mem) -> 
      {
         for (int i = 0; i < caller.length; i++)
         {
            if (!ignore[i])
            {
               if (caller[i].mode != null)
               {
                  if (!caller[i].equals(mem.data.getSignature(i)))
                  {
                     return false;
                  }
               }
               else
               {
                  if (!caller[i].typeEquivalent(mem.data.getSignature(i)))
                  {
                     return false;
                  }
               }
            }
         }
         
         return true;
      };
      
      MethodSearchResult result = null;
      boolean       flex   = false;
      
      if (exact.isEmpty())
      {
         List<MatchMetrics> flexMode = list.stream().filter(flexModeMatch).collect(Collectors.toList());
         
         if (flexMode.size() == 1)
         {
            result = new MethodSearchResult(flexMode.get(0).data, null);
            flex   = true;
         }
      }
      else
      {
         if (exact.size() == 1)
         {
            result = new MethodSearchResult(exact.get(0).data, null);
         }
      }
      
      if (result != null)
      {
         // table-handle/dataset-handle matches are only valid when the only difference in signature is
         // between a table/dataset and table-handle/dataset-handle

         Iterator<MatchMetrics> iter = list.iterator();
         
         // calculate which parameters are both table-handles/dataset-handles and have conflicting
         // signature (another signature that has a table/dataset in the same parameter position)
         while (iter.hasNext())
         {
            MatchMetrics m = iter.next();
            
            for (int i = 0; i < caller.length; i++)
            {
               // quick iteration if we already know this parameter should be ignored 
               if (ignore[i])
                  continue;
               
               String type = m.data.getSignature(i).type;

               if ("TABLE-HANDLE".equals(caller[i].type))
               {
                  if (type != null && type.startsWith("TEMP-TABLE"))
                  {
                     ignore[i] = true;
                  }
               }
               else if ("DATASET-HANDLE".equals(caller[i].type))
               {
                  if (type != null && type.startsWith("DATASET"))
                  {
                     ignore[i] = true;
                  }
               }
            }
         }

         // ignoring the conflicting parameters, can we still calculate find a unique match?
         List<MatchMetrics> ignoreTH = list.stream()
                                            .filter(flex ? flexModeMatch : exactMatch)
                                            .collect(Collectors.toList());
         if (ignoreTH.size() != 1)
         {
            result = null;
         }
      }
      
      return result;
   }
   
   /**
    * Detect if any extent specification in the parameters are a fuzzy match or if neither have
    * an extent specification.
    * <p>
    * It is assumed that any exact matches are handled elsewhere.
    *
    * @param    defSig
    *           Definition parameter type spec.
    * @param    callSig
    *           Call parameter type spec.
    *
    * @return   {@code true} if a fixed extent call parm is passed to an indeterminate extent def
    *           parm or if neither have an extent spec.
    */
   private boolean isExtentMatch(String defSig, String callSig)
   {
      if (!defSig.endsWith("]") && !callSig.endsWith("]"))
      {
         // neither spec has extent
         return true;
      }
      
      if (defSig.endsWith("[]") && callSig.endsWith("]"))
      {
         // this is a fixed (or indeterminate) extent passed to an indeterminate extent
         return true;
      }
      
      return false;
   }
   
   /**
    * Get the list of all possible methods that could match in this class AND its parent classes. The
    * actual types are not checked but the method name, access mode, static/instance and number of
    * parameters must all match.
    *
    * @param    name
    *           Method name.
    * @param    num
    *           Number of parameters.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up. Otherwise any method can be
    *           returned.
    * @param    internal
    *           {@code true} if the lookup is internal to the current class definition.
    * @param    first
    *           {@code true} if the first match should be immediately returned as the result.
    * @param    exist
    *           The set of existing methods already represented in the list.
    * @param    constructor
    *           Flag indicating this must resolve a constructor.
    *
    * @return   List of possible methods that match.  This may be an empty list if no match exists.
    */
   List<MatchMetrics> candidates(String            name,
                                 int               num,
                                 int               access,
                                 boolean           isStatic,
                                 boolean           internal,
                                 boolean           first,
                                 Set<SignatureKey> exist,
                                 boolean           constructor)
   {
      Set<SignatureKey>   existing = exist == null ? new HashSet<>() : exist;
      List<MatchMetrics> results  = new LinkedList<>();
      
      synchronized (lock)
      {
         Map<String, Map<SignatureKey, MemberData>> map = isStatic ? smethods : imethods;
         
         // if the method name matches this class method name, then it can't be part of this class 
         // (4gl compile error) 
         if (map != null && (constructor || !name.equalsIgnoreCase(this.getSimpleName())))
         {
            Map<SignatureKey, MemberData> defs = map.get(name.toLowerCase());
            
            if (defs != null)
            {
               if (first)
               {
                  results.add(new MatchMetrics(defs.values().iterator().next()));
                  
                  return results;
               }
               
               Iterator<MemberData> iter = defs.values().iterator();
               
               while (iter.hasNext())
               {
                  MemberData dat = checkAccessRights(iter.next(), access);
                  
                  if (dat != null && dat.signature.length == num)
                  {
                     // special case: no parameters; this is Highlander (i.e. "there can be only one")
                     // and we don't have to search the parents, this is the match
                     if (num == 0)
                     {
                        results.add(new MatchMetrics(dat));
                        return results;
                     }
                     
                     SignatureKey key = new SignatureKey(dat.signature);
                     
                     if (!existing.contains(key))
                     {
                        results.add(new MatchMetrics(dat));
                        existing.add(key);
                     }
                  }
               }
            }
         }
         
         // fuzzy matches are processed up the the parent hierarchy even if there are possible matches in
         // the current class
         if (parents != null)
         {
            // change access mode since we aren't allowed to see private stuff in parent
            int _access = (access == KW_PRIVATE) ? KW_PROTECTD : access;
            
            for (ClassDefinition parent : parents)
            {
               // recurse up inheritance hierarchy
               results.addAll(parent.candidates(name, num, _access, isStatic, internal, first, existing, 
                                                false));
            }
         }
      }
      
      return results;
   }

   /**
    * Detect if the given class is an instance of an implemented interface of this class.
    * 
    * @param    cls
    *           The class to check.
    *           
    * @return   {@code true} if the given class is an implemented interface in this class. 
    */
   boolean isImplemented(ClassDefinition cls)
   {
      if (ifaces == null)
      {
         return false;
      }
      
      return ifaces.contains(cls.getName()) || ifaces.contains(cls.getName().toLowerCase());
   }
   
   /**
    * Detect if the given class is an instance of a parent class or an implemented interface of a parent
    * class.
    * 
    * @param    cls
    *           The class to check.
    * @param    ifaces
    *           {@code true} if implemented interfaces should be checked.
    * @param    incrementer
    *           Code to be executed to increment the inheritance level.
    *
    * @return   {@code true} if the given class is a parent class or an implemented interface of a parent
    *           class. 
    */
   boolean isInheritedFrom(ClassDefinition cls, boolean ifaces, Runnable incrementer)
   {
      if (parents != null)
      {
         for (ClassDefinition parent : parents)
         {
            if (cls == parent || (ifaces && parent.isImplemented(cls)))
            {
               // track the current parent level
               incrementer.run();

               return true;
            }
            
            if (parent.isInheritedFrom(cls, ifaces, incrementer))
            {
               // track our level's contribution to the parent level
               incrementer.run();
               
               return true;
            }
         }
      }
      
      if (ifaces && interfaces != null)
      {
         for (ClassDefinition parent : interfaces)
         {
            if (cls == parent || parent.isImplemented(cls))
            {
               // track the current parent level
               incrementer.run();

               return true;
            }
         }
      }
      
      return false;
   }

   /**
    * Confirm that the given member is accessible at the access level specified.
    *
    * @param    mdat
    *           Member data, may be {@code null}.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or
    *           <code>KW_PRIVATE</code>).
    *
    * @return   The given resource if accessible or <code>null</code> access is not allowed or
    *           if the member is {@code null} on input.
    */
   private MemberData checkAccessRights(MemberData mdat, int access)
   {
      if (mdat != null)
      {
         // check for a match to the access mode:
         // 1. searching for private methods match everything
         // 2. a direct match is always OK
         // 3. a public method matches a protected or package private search
         boolean ok = (access == KW_PRIVATE)  ||
                      (access == mdat.access) ||
                      ((access == KW_PROTECTD || access == KW_PK_PRIV) && mdat.access == KW_PUBLIC);
         
         // pretend no match if access modes are not right
         if (!ok)
         {
            return null;
         }
      }
      
      return mdat;
   }
   
   /**
    * Add the resource to the given maps, if it is not already present OR if it is present
    * under a more restrictive access type (we must store the least restrictive of the types
    * to allow operator overloading to work properly).
    *
    * @param    stype
    *           Type of data store to check.
    * @param    name
    *           Resource name.
    * @param    signature
    *           Parameter signature for methods or <code>null</code>.
    * @param    var
    *           Variable reference for var resources or <code>null</code>.
    * @param    type
    *           Token type.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the variable/property is static.
    * @param    qname
    *           Fully qualified class name where the resource represents an
    *           object instance, otherwise <code>null</code>.
    * @param    tempIdx
    *           Temporary ID.
    * @return   The found {@link MemberData} instance.
    */
   private MemberData addWorker(DataStoreType  stype,
                                String         name,
                                ParameterKey[] signature,
                                Variable       var,
                                int            type,
                                int            access,
                                boolean        isStatic,
                                String         qname,
                                int            tempIdx)
   {
      synchronized (lock)
      {
         Map<String, MemberData> map   = getMemberStore(stype, isStatic);
         String                  lower = name.toLowerCase();
         MemberData              odat  = map.get(lower);
         
         boolean insert = true;
         
         if (odat != null)
         {
            // a record already exists, only replace it if we are making it more accessible
            if ((odat.access == KW_PRIVATE && (access == KW_PUBLIC || access == KW_PROTECTD)) ||
                (odat.access == KW_PROTECTD && access == KW_PUBLIC))
            {
               // leave insert as true
            }
            else
            {
               insert = false;
            }
         }
         
         if (insert)
         {
            MemberData mdat = new MemberData(name,
                                             var,
                                             signature,
                                             type,
                                             access,
                                             isStatic,
                                             qname,
                                             this,
                                             tempIdx,
                                             null);
            
            // add/replace in the full member list
            map.put(lower, mdat);
            
            return mdat;
         }
         else
         {
            // replace the var with the new one
            if (odat.var != null)
            {
               var.setTempIndex(odat.var.getTempIndex());
               odat.var = var;
            }
         }
         
         return odat;
      }
   }
   
   /**
    * Find the named resource in the given map.
    *
    * @param    map
    *           Dictionary for all resources regardless of access mode.
    * @param    name
    *           Resource name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    *
    * @return   Resource found or <code>null</code> if no match exists.
    */
   private MemberData lookupWorker(Map<String, MemberData> map, String name, int access)
   {
      MemberData mdat = null;
      
      synchronized (lock)
      {
         if (map != null)
         {
            mdat = checkAccessRights(map.get(name.toLowerCase()), access);
         }
      }
      
      return mdat;
   }
   
   /**
    * Pull out our lists of temp-tables and buffers so they can be used for
    * cleanup.
    *
    * @param    save
    *           Set of temp-tables and buffers.
    * @param    isStatic
    *           <code>true</code> to process static resources, otherwise instance resources.
    */
   private void defineExceptions(ArrayList<String> save, boolean isStatic)
   {
      Map<String, MemberData> map = getMemberStore(DataStoreType.TABLE, isStatic);
      
      if (map != null && !map.isEmpty())
      {
         Iterator<String> iter = map.keySet().iterator();
         
         while (iter.hasNext())
         {
            MemberData mdat = map.get(iter.next());
            
            if (mdat != null)
            {
               if ((mdat.type == BUFFER || mdat.type == TEMP_TABLE) && mdat.access == KW_PROTECTD)
               {
                  save.add(mdat.name);
               }
            }
         }
      }
   }
   
   /**
    * Add the 'virtual' enum methods, so they can be resolved by the parser.  This includes the static
    * {@code GetEnum} method for all enums and the instance {@code SetFlag}, {@code ToggleFlag} and
    * {@code UnsetFlag} for flags enums.
    *
    * @param    name
    *           Fully qualified class name of this enum.
    * @param    flags
    *           {@code true} if this is a flags enum.
    */
   private void registerVirtualEnumMethods(String name, boolean flags)
   {
      ParameterKey[] sig1 = new ParameterKey[] { new ParameterKey(KW_INPUT, "int64") };
      registerVirtual(name, "GetEnum", "getEnum", true, OO_METH_CLASS, name, sig1);
      
      ParameterKey[] sig2 = new ParameterKey[] { new ParameterKey(KW_INPUT, "character") };
      registerVirtual(name, "GetEnum", "getEnum", true, OO_METH_CLASS, name, sig2);
      
      if (flags)
      {
         ParameterKey[] sig3 = new ParameterKey[]
         {
            new ParameterKey(KW_INPUT, String.format("object <? extends %s>", name))
         };
         
         registerVirtual(name, "SetFlag", "setFlag", false, OO_METH_CLASS, name, sig3);
         registerVirtual(name, "ToggleFlag", "toggleFlag", false, OO_METH_CLASS, name, sig3);
         registerVirtual(name, "UnsetFlag", "unsetFlag", false, OO_METH_CLASS, name, sig3);
      }
   }
   
   /**
    * Helper method to register a public virtual method.
    *
    * @param    name
    *           Fully qualified class name.
    * @param    ori
    *           Legacy method name.
    * @param    meth
    *           Converted method name.
    * @param    isStatic
    *           {@code true} if this is a static method, {@code false} for instance methods.
    * @param    rtype
    *           Method's return token type.
    * @param    qname
    *           Fully qualified OO name of the return type or {@code null} for non-object return types.
    * @param    parms
    *           The signature details.
    */
   private void registerVirtual(String         name,
                                String         ori,
                                String         meth,
                                boolean        isStatic,
                                int            rtype,
                                String         qname,
                                ParameterKey[] parms)
   {
      // the method must be registered
      createMethod(null, ori, parms, KW_PUBLIC, true, rtype, qname, meth, 0, nextTempIdx(), false, false, 
                   null, true);
   }
   
   /**
    * Create a new method definition and track it.
    *  
    * @param    name
    *           The legacy method name.
    * @param    sig
    *           The method signature.
    * @param    accessMode
    *           The access mode.
    * @param    isStatic
    *           Flag indicating if the method is static or not.
    * @param    retType
    *           The return type.
    * @param    qualified
    *           The qualified legacy class name, for methods returning object.
    * @param    javaname
    *           The converted java name.
    * @param    extent
    *           The extent (for the return type).
    * @param    tempIdx
    *           The tempIdx to use.
    * @param    override
    *           {@code true} if the method is an override of a parent class/interface method.
    * @param    abstr
    *           {@code true} if the method is abstract of a parent class/interface method.
    *           
    * @return   The created method.
    */
   private MemberData createMethod(Map<SignatureKey, MemberData> defs,
                                   String                        name,
                                   ParameterKey[]                sig, 
                                   int                           accessMode, 
                                   boolean                       isStatic,
                                   int                           retType,
                                   String                        qualified,
                                   String                        javaname,
                                   int                           extent,
                                   int                           tempIdx,
                                   boolean                       override,
                                   boolean                       abstr)
   {
      return createMethod(defs, name, sig, accessMode, isStatic, retType, qualified,
                          javaname, extent, tempIdx, override, abstr, null, false);
   }
   
   /**
    * Create a new method definition and track it.
    *  
    * @param    name
    *           The legacy method name.
    * @param    sig
    *           The method signature.
    * @param    accessMode
    *           The access mode.
    * @param    isStatic
    *           Flag indicating if the method is static or not.
    * @param    retType
    *           The return type.
    * @param    qualified
    *           The qualified legacy class name, for methods returning object.
    * @param    javaname
    *           The converted java name.
    * @param    extent
    *           The extent (for the return type).
    * @param    tempIdx
    *           The tempIdx to use.
    * @param    override
    *           {@code true} if the method is an override of a parent class/interface method.
    * @param    abstr
    *           {@code true} if the method is abstract of a parent class/interface method.
    * @param    register
    *           {@code true} if the name should be registered in the maps.
    *           
    * @return   The created method.
    */
   private MemberData createMethod(Map<SignatureKey, MemberData> defs,
                                   String                        name,
                                   ParameterKey[]                sig, 
                                   int                           accessMode, 
                                   boolean                       isStatic,
                                   int                           retType,
                                   String                        qualified,
                                   String                        javaname,
                                   int                           extent,
                                   int                           tempIdx,
                                   boolean                       override,
                                   boolean                       abstr,
                                   Map<String, Object>           ret,
                                   boolean                       register)
   {
      synchronized (lock)
      {
         if (defs == null)
         {
            defs = getMethodStore(name, isStatic);
         }
         
         MemberData mdat = new MemberData(name, 
                                          null, 
                                          sig, 
                                          retType, 
                                          accessMode,
                                          isStatic, 
                                          qualified, 
                                          this, 
                                          tempIdx,  
                                          ret);
         mdat.extent   = extent;
         mdat.override = override;
         mdat.abstr = abstr;
         
         defs.put(new SignatureKey(sig), mdat);
         
         if (register)
         {
            registerMethod(javaname, mdat);
         }
         
         return mdat;
      }
   }
}