# 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;
          }
       }
    }