text: /**
   * Copyright 2013-2021 the original author or authors from the JHipster project.
   *
   * This file is part of the JHipster project, see https://www.jhipster.tech/
   * for more information.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      https://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  const JDLParser = require('./jdl-parser');
  const { deduplicate } = require('../utils/array-utils');
  
  const parser = JDLParser.getParser();
  parser.parse();
  
  const BaseJDLCSTVisitor = parser.getBaseCstVisitorConstructor();
  
  module.exports = class JDLAstBuilderVisitor extends BaseJDLCSTVisitor {
      constructor() {
          super();
          this.validateVisitor();
      }
  
      prog(context) {
          const ast = {
              applications: [],
              deployments: [],
              constants: {},
              entities: [],
              relationships: [],
              enums: [],
              options: {},
              useOptions: [],
          };
  
          if (context.constantDeclaration) {
              const constants = context.constantDeclaration.map(this.visit, this);
              constants.forEach(currConst => {
                  ast.constants[currConst.name] = currConst.value;
              });
          }
  
          if (context.applicationDeclaration) {
              ast.applications = context.applicationDeclaration.map(this.visit, this);
          }
  
          if (context.deploymentDeclaration) {
              ast.deployments = context.deploymentDeclaration.map(this.visit, this);
          }
  
          if (context.entityDeclaration) {
              ast.entities = context.entityDeclaration.map(this.visit, this);
          }
  
          if (context.relationDeclaration) {
              ast.relationships = context.relationDeclaration.flatMap(this.visit, this);
          }
  
          if (context.enumDeclaration) {
              ast.enums = context.enumDeclaration.map(this.visit, this);
          }
  
          if (context.unaryOptionDeclaration) {
              context.unaryOptionDeclaration.map(this.visit, this).forEach(option => {
                  if (!ast.options[option.optionName]) {
                      ast.options[option.optionName] = {};
                  }
                  const astResult = ast.options[option.optionName];
  
                  const { entityList, excludedEntityList } = getOptionEntityAndExcludedEntityLists(astResult, option);
                  astResult.list = entityList;
                  astResult.excluded = excludedEntityList;
              });
          }
  
          if (context.binaryOptionDeclaration) {
              context.binaryOptionDeclaration.map(this.visit, this).forEach(option => {
                  if (option.optionName === 'paginate') {
                      option.optionName = 'pagination';
                  }
                  const newOption = !ast.options[option.optionName];
                  if (newOption) {
                      ast.options[option.optionName] = {};
                  }
                  const newOptionValue = !ast.options[option.optionName][option.optionValue];
                  if (newOptionValue) {
                      ast.options[option.optionName][option.optionValue] = {};
                  }
                  const astResult = ast.options[option.optionName][option.optionValue];
  
                  const { entityList, excludedEntityList } = getOptionEntityAndExcludedEntityLists(astResult, option);
                  astResult.list = entityList;
                  astResult.excluded = excludedEntityList;
              });
          }
  
          if (context.useOptionDeclaration) {
              ast.useOptions = context.useOptionDeclaration.map(this.visit, this);
          }
  
          return ast;
      }
  
      constantDeclaration(context) {
          return {
              name: context.NAME[0].image,
              value: context.INTEGER ? context.INTEGER[0].image : context.DECIMAL[0].image,
          };
      }
  
      entityDeclaration(context) {
          const annotations = [];
          if (context.annotationDeclaration) {
              context.annotationDeclaration.forEach(contextObject => {
                  annotations.push(this.visit(contextObject));
              });
          }
  
          let javadoc = null;
          if (context.JAVADOC) {
              javadoc = trimComment(context.JAVADOC[0].image);
          }
  
          const name = context.NAME[0].image;
  
          let tableName = name;
          if (context.entityTableNameDeclaration) {
              tableName = this.visit(context.entityTableNameDeclaration);
          }
  
          let body = [];
          if (context.entityBody) {
              body = this.visit(context.entityBody);
          }
  
          return {
              annotations,
              name,
              tableName,
              body,
              javadoc,
          };
      }
  
      annotationDeclaration(context) {
          if (!context.value) {
              return { optionName: context.option[0].image, type: 'UNARY' };
          }
          return {
              optionName: context.option[0].image,
              optionValue: context.value[0].image.replace(/"/g, ''),
              type: 'BINARY',
          };
      }
  
      entityTableNameDeclaration(context) {
          return context.NAME[0].image;
      }
  
      entityBody(context) {
          if (!context.fieldDeclaration) {
              return [];
          }
          return context.fieldDeclaration.map(element => this.visit(element));
      }
  
      fieldDeclaration(context) {
          const annotations = [];
          if (context.annotationDeclaration) {
              context.annotationDeclaration.forEach(contextObject => {
                  annotations.push(this.visit(contextObject));
              });
          }
  
          // filter actual comment as the comment rule may be empty
          const comment = context.JAVADOC ? trimComment(context.JAVADOC[0].image) : null;
  
          let validations = [];
          if (context.validation) {
              validations = context.validation.map(element => this.visit(element));
          }
  
          return {
              name: context.NAME[0].image,
              // context.type is an array with a single item.
              // in that case:
              // this.visit(context.type) is equivalent to this.visit(context.type[0])
              type: this.visit(context.type),
              validations,
              javadoc: comment,
              annotations,
          };
      }
  
      type(context) {
          return context.NAME[0].image;
      }
  
      validation(context) {
          // only one of these alternatives can exist at the same time.
          if (context.REQUIRED) {
              return {
                  key: 'required',
                  value: '',
              };
          }
          if (context.UNIQUE) {
              return {
                  key: 'unique',
                  value: '',
              };
          }
          if (context.minMaxValidation) {
              return this.visit(context.minMaxValidation);
          }
          return this.visit(context.pattern);
      }
  
      minMaxValidation(context) {
          if (context.NAME) {
              return {
                  key: context.MIN_MAX_KEYWORD[0].image,
                  value: context.NAME[0].image,
                  constant: true,
              };
          }
  
          return {
              key: context.MIN_MAX_KEYWORD[0].image,
              value: context.INTEGER ? context.INTEGER[0].image : context.DECIMAL[0].image,
          };
      }
  
      pattern(context) {
          const patternImage = context.REGEX[0].image;
  
          return {
              key: 'pattern',
              value: patternImage.substring(1, patternImage.length - 1),
          };
      }
  
      relationDeclaration(context) {
          const cardinality = this.visit(context.relationshipType);
          const relationshipBodies = context.relationshipBody.map(this.visit, this);
  
          relationshipBodies.forEach(relationshipBody => {
              relationshipBody.cardinality = cardinality;
          });
  
          return relationshipBodies;
      }
  
      relationshipType(context) {
          return context.RELATIONSHIP_TYPE[0].image;
      }
  
      relationshipBody(context) {
          const optionsForTheSourceSide = context.annotationOnSourceSide ? context.annotationOnSourceSide.map(this.visit, this) : [];
          const optionsForTheDestinationSide = context.annotationOnDestinationSide
              ? context.annotationOnDestinationSide.map(this.visit, this)
              : [];
  
          const from = this.visit(context.from);
          const to = this.visit(context.to);
  
          const relationshipOptions = [];
          if (context.relationshipOptions) {
              this.visit(context.relationshipOptions).forEach(option => relationshipOptions.push(option));
          }
  
          return {
              from,
              to,
              options: {
                  global: relationshipOptions,
                  source: optionsForTheSourceSide,
                  destination: optionsForTheDestinationSide,
              },
          };
      }
  
      relationshipSide(context) {
          const javadoc = this.visit(context.comment);
          const name = context.NAME[0].image;
  
          const required = !!context.REQUIRED;
          let injectedField = null;
  
          if (context.injectedField) {
              injectedField = context.injectedField[0].image;
  
              if (context.injectedFieldParam) {
                  injectedField += `(${context.injectedFieldParam[0].image})`;
              }
          }
  
          const ast = {
              name,
              injectedField,
              javadoc,
              required,
          };
  
          if (!injectedField) {
              delete ast.required;
          }
          return ast;
      }
  
      relationshipOptions(context) {
          return context.relationshipOption.map(this.visit, this).reduce((final, current) => [...final, current], []);
      }
  
      relationshipOption(context) {
          if (context.JPA_DERIVED_IDENTIFIER) {
              return { optionName: 'jpaDerivedIdentifier', type: 'UNARY' };
          }
  
          /* istanbul ignore next */
          throw new Error("No valid relationship option found, expected 'jpaDerivedIdentifier'.");
      }
  
      enumDeclaration(context) {
          const name = context.NAME[0].image;
          const values = this.visit(context.enumPropList);
  
          return { name, values };
      }
  
      enumPropList(context) {
          return context.enumProp.map(this.visit, this);
      }
  
      enumProp(context) {
          const prop = {
              key: context.enumPropKey[0].image,
          };
          if (context.enumPropValue) {
              prop.value = context.enumPropValue[0].image;
          }
          if (context.enumPropValueWithQuotes) {
              prop.value = context.enumPropValueWithQuotes[0].image.replace(/"/g, '');
          }
          return prop;
      }
  
      entityList(context) {
          let entityList = [];
          if (context.NAME) {
              entityList = context.NAME.map(nameToken => nameToken.image);
          }
  
          const entityOnlyListContainsAll = entityList.length === 1 && entityList[0] === 'all';
  
          if (context.STAR || entityOnlyListContainsAll) {
              entityList = ['*'];
          }
  
          if (context.method) {
              entityList.push(context.method[0].image);
          }
          if (context.methodPath) {
              entityList.push(context.methodPath[0].image);
          }
  
          return deduplicate(entityList);
      }
  
      exclusion(context) {
          return context.NAME.map(nameToken => nameToken.image, this);
      }
  
      unaryOptionDeclaration(context) {
          return getUnaryOptionFromContext(context, this);
      }
  
      binaryOptionDeclaration(context) {
          return getBinaryOptionFromContext(context, this);
      }
  
      useOptionDeclaration(context) {
          return getSpecialUnaryOptionDeclaration(context, this);
      }
  
      filterDef(context) {
          let entityList = [];
          if (context.NAME) {
              entityList = context.NAME.map(nameToken => nameToken.image, this);
          }
  
          const entityOnlyListContainsAll = entityList.length === 1 && entityList[0] === 'all';
  
          if (context.STAR || entityOnlyListContainsAll) {
              entityList = ['*'];
          }
  
          return deduplicate(entityList);
      }
  
      comment(context) {
          if (context.JAVADOC) {
              return trimComment(context.JAVADOC[0].image);
          }
  
          return null;
      }
  
      deploymentDeclaration(context) {
          const config = {};
  
          if (context.deploymentConfigDeclaration) {
              const configProps = context.deploymentConfigDeclaration.map(this.visit, this);
              configProps.forEach(configProp => {
                  config[configProp.key] = configProp.value;
              });
          }
  
          return config;
      }
  
      deploymentConfigDeclaration(context) {
          const key = context.DEPLOYMENT_KEY[0].image;
          const value = this.visit(context.deploymentConfigValue);
  
          return { key, value };
      }
  
      deploymentConfigValue(context) {
          return this.configValue(context);
      }
  
      applicationDeclaration(context) {
          return this.visit(context.applicationSubDeclaration);
      }
  
      applicationSubDeclaration(context) {
          const applicationSubDeclaration = {
              config: {},
              entities: { entityList: [], excluded: [] },
              options: {},
              useOptions: [],
          };
  
          if (context.applicationSubConfig) {
              // Apparently the pegjs grammar only returned the last config
              applicationSubDeclaration.config = this.visit(context.applicationSubConfig[context.applicationSubConfig.length - 1]);
          }
  
          if (context.applicationSubEntities) {
              // Apparently the pegjs grammar only returned the last entities
              applicationSubDeclaration.entities = this.visit(context.applicationSubEntities[context.applicationSubEntities.length - 1]);
          }
  
          if (context.unaryOptionDeclaration) {
              context.unaryOptionDeclaration.map(this.visit, this).forEach(option => {
                  if (!applicationSubDeclaration.options[option.optionName]) {
                      applicationSubDeclaration.options[option.optionName] = {};
                  }
                  const astResult = applicationSubDeclaration.options[option.optionName];
  
                  const { entityList, excludedEntityList } = getOptionEntityAndExcludedEntityLists(astResult, option);
                  astResult.list = entityList;
                  astResult.excluded = excludedEntityList;
              });
          }
  
          if (context.binaryOptionDeclaration) {
              context.binaryOptionDeclaration.map(this.visit, this).forEach(option => {
                  if (option.optionName === 'paginate') {
                      option.optionName = 'pagination';
                  }
                  const newOption = !applicationSubDeclaration.options[option.optionName];
                  if (newOption) {
                      applicationSubDeclaration.options[option.optionName] = {};
                  }
                  const newOptionValue = !applicationSubDeclaration.options[option.optionName][option.optionValue];
                  if (newOptionValue) {
                      applicationSubDeclaration.options[option.optionName][option.optionValue] = {};
                  }
                  const astResult = applicationSubDeclaration.options[option.optionName][option.optionValue];
  
                  const { entityList, excludedEntityList } = getOptionEntityAndExcludedEntityLists(astResult, option);
                  astResult.list = entityList;
                  astResult.excluded = excludedEntityList;
              });
          }
  
          if (context.useOptionDeclaration) {
              context.useOptionDeclaration.map(this.visit, this).forEach(option => {
                  applicationSubDeclaration.useOptions.push(option);
              });
          }
  
          return applicationSubDeclaration;
      }
  
      applicationSubConfig(context) {
          const config = {};
  
          if (context.applicationConfigDeclaration) {
              const configProps = context.applicationConfigDeclaration.map(this.visit, this);
              configProps.forEach(configProp => {
                  config[configProp.key] = configProp.value;
  
                  if (configProp.key === 'packageName' && !config.packageFolder) {
                      config.packageFolder = configProp.value.replace(/[.]/g, '/');
                  }
              });
          }
  
          return config;
      }
  
      applicationSubEntities(context) {
          return getEntityListFromContext(context, this);
      }
  
      applicationConfigDeclaration(context) {
          const key = context.CONFIG_KEY[0].image;
          const value = this.visit(context.configValue);
  
          return { key, value };
      }
  
      configValue(context) {
          if (context.qualifiedName) {
              return this.visit(context.qualifiedName);
          }
          if (context.list) {
              return this.visit(context.list);
          }
          if (context.INTEGER) {
              return context.INTEGER[0].image;
          }
          if (context.STRING) {
              const stringImage = context.STRING[0].image;
              return stringImage.substring(1, stringImage.length - 1);
          }
          if (context.BOOLEAN) {
              return context.BOOLEAN[0].image === 'true';
          }
  
          /* istanbul ignore next */
          throw new Error('No valid config value was found, expected a qualified name, a list, an integer, a string or a boolean.');
      }
  
      qualifiedName(context) {
          return context.NAME.map(namePart => namePart.image, this).join('.');
      }
  
      list(context) {
          if (!context.NAME) {
              return [];
          }
          return context.NAME.map(namePart => namePart.image, this);
      }
  };
  
  function getOptionEntityAndExcludedEntityLists(astResult, option) {
      let entityList = astResult.list || [];
      entityList = deduplicate(entityList.concat(option.list));
  
      let excludedEntityList = astResult.excluded || [];
      if (option.excluded) {
          excludedEntityList = deduplicate(excludedEntityList.concat(option.excluded));
      }
      return { entityList, excludedEntityList };
  }
  
  function getEntityListFromContext(context, visitor) {
      const entityList = visitor.visit(context.filterDef);
  
      let excluded = [];
      if (context.exclusion) {
          excluded = visitor.visit(context.exclusion);
      }
  
      const result = { entityList, excluded };
      if (context.UNARY_OPTION) {
          result.optionName = context.UNARY_OPTION[0].image;
      }
      return result;
  }
  
  function getUnaryOptionFromContext(context, visitor) {
      const entityList = visitor.visit(context.filterDef);
  
      let excluded = [];
      if (context.exclusion) {
          excluded = visitor.visit(context.exclusion);
      }
  
      return { optionName: context.UNARY_OPTION[0].image, list: entityList, excluded };
  }
  
  function getBinaryOptionFromContext(context, visitor) {
      const entityListWithOptionValue = visitor.visit(context.entityList);
      const optionValue = entityListWithOptionValue[entityListWithOptionValue.length - 1];
      const list = entityListWithOptionValue.slice(0, entityListWithOptionValue.length - 1);
  
      let excluded = [];
      if (context.exclusion) {
          excluded = visitor.visit(context.exclusion);
      }
  
      return {
          optionName: context.BINARY_OPTION[0].image,
          optionValue,
          list,
          excluded,
      };
  }
  
  function getSpecialUnaryOptionDeclaration(context, visitor) {
      const optionValues = context.NAME.map(name => name.image);
      const list = visitor.visit(context.filterDef);
  
      let excluded = [];
      if (context.exclusion) {
          excluded = visitor.visit(context.exclusion);
      }
  
      return {
          optionValues,
          list,
          excluded,
      };
  }
  
  function trimComment(comment) {
      return comment.replace(/^\/[*]+/, '').replace(/[*]+\/$/, '');
  }
  