All files / src/rules constant_classes.ts

90.29% Statements 158/175
87.8% Branches 36/41
100% Functions 7/7
90.29% Lines 158/175

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 1751x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7684x 7684x 7684x 7684x 7684x 96529x 96529x 96529x 96529x 96529x 96529x 96529x 96529x 96529x 7684x 7684x 152x 152x 152x 7684x 7684x 7364x 7364x 7684x 7684x 152x 152x 7684x 7684x 178x 178x 178x 12x 12x     12x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 11x 12x     11x 12x         11x 11x 11x 11x 11x 12x                   11x 11x 11x 11x 12x 40x 40x 40x 2x 2x 2x 1x 1x 1x 1x 2x 2x 2x 38x 40x 4x 4x 4x 4x 4x 38x 40x 2x 2x 2x 2x 2x 38x 40x 1x 1x 1x 1x 1x 40x 11x 12x 44x 7x 7x 7x 7x 7x 7x 7x 7x 7x 44x 11x 12x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 11x 166x 166x 166x 7684x 7684x 8x 8x 8x 8x 8x 8x 7684x
import {Issue} from "../issue";
import {BasicRuleConfig} from "./_basic_rule_config";
import {IRule, IRuleMetadata, RuleTag} from "./_irule";
import {IObject, IRegistry, Objects, Visibility} from "..";
import {InfoConstant} from "../abap/4_file_information/_abap_file_information";
import {Class} from "../objects";
 
export interface DomainClassMapping {
  /** Domain name. The domain must have fixed values. */
  domain: string,
  /** Class name */
  class: string,
  /** Ensure the type of the constant is an exact match of the domain name. */
  useExactType?: boolean,
  /** Specify additional constant name containing the domain name (optional).
   * A domain name constant is preferable to using a hardcoded value as the usage can be located by a where-used-list */
  constantForDomainName?: string,
}
 
/** Checks that constants classes are in sync with domain fixed values */
export class ConstantClassesConf extends BasicRuleConfig {
  /** Specify a list of domain-class pairs which will be validated */
  public mapping: DomainClassMapping[];
}
 
export class ConstantClasses implements IRule {
  private conf = new ConstantClassesConf();
  private reg: IRegistry;
 
  public getMetadata(): IRuleMetadata {
    return {
      key: "constant_classes",
      title: "Validate constant classes",
      shortDescription: `Checks that a class contains exactly the constants corresponding to a domain's fixed values.`,
      extendedInformation:
        `https://github.com/SAP/styleguides/blob/main/clean-abap/CleanABAP.md#prefer-enumeration-classes-to-constants-interfaces`,
      tags: [RuleTag.Syntax, RuleTag.Styleguide, RuleTag.Experimental],
    };
  }
 
  public initialize(reg: IRegistry) {
    this.reg = reg;
    return this;
  }
 
  public getConfig() {
    return this.conf;
  }
 
  public setConfig(conf: ConstantClassesConf) {
    this.conf = conf;
  }
 
  public run(obj: IObject): Issue[] {
    if (this.conf
      && this.conf.mapping
      && obj instanceof Objects.Domain) {
      const configEntry = this.conf.mapping.find(x => x.domain.toUpperCase() === obj.getName().toUpperCase());
      if (!configEntry) {
        return [];
      }
 
      const classWithConstants = this.reg.getObject("CLAS", configEntry?.class.toUpperCase()) as Class | undefined;
      if (!classWithConstants) {
        return [Issue.atIdentifier(
          obj.getIdentifier()!,
          `Constant class pattern implementation ${configEntry.class} missing for domain ${configEntry.domain}`,
          this.getMetadata().key,
          this.conf.severity)];
 
        // quickfix will implement the whole class
      }
      const classContents = classWithConstants.getMainABAPFile();
      if (classContents === undefined) {
        return [];
      }
      const def = classWithConstants.getClassDefinition();
      if (!def) {
        // this issue is checked by rule implement_methods.
        // we will not issue errors that all constants are missing until there is a class implementation
        return [];
      }
 
      const domainValueInfo = obj.getFixedValues();
      const domainValues = domainValueInfo.map(x => x.value);
      const issues: Issue[] = [];
 
      if (obj.getFixedValues().length === 0) {
        // possibly this is not even a domain with fixed values
        issues.push(
          Issue.atStatement(
            classContents,
            classContents.getStatements()[0],
            `Domain ${configEntry.domain} does not contain any fixed values. Either add some values or disable this check`,
            this.getMetadata().key,
            this.conf.severity));
      }
 
      // later we will raise an issue if we did not find it
      let domainNameConstantFound = false;
 
      for (const constant of def.constants) {
 
        if (configEntry.constantForDomainName
          && constant.name === configEntry.constantForDomainName) {
          // we require the constant value to be uppercase just in case
          // in the config it does not matter
          if (constant.value !== configEntry.domain.toLocaleUpperCase()) {
            issues.push(this.issueAtConstant(
              constant,
              `Constant value ${constant.value} must match domain name ${configEntry.domain} `));
          }
          domainNameConstantFound = true;
          continue;
        }
 
        if (configEntry.useExactType && constant.typeName.toLowerCase() !== configEntry.domain.toLowerCase()) {
          issues.push(this.issueAtConstant(
            constant,
            `Use exact type ${configEntry.domain} instead of ${constant.typeName}`));
          // quickfix will change the type
        }
 
        if (constant.visibility !== Visibility.Public) {
          issues.push(this.issueAtConstant(
            constant,
            `Constant ${constant.name} should be public`));
          // quickfix will move constant
        }
 
        if (!domainValues.includes(constant.value)) {
          issues.push(this.issueAtConstant(
            constant,
            `Extra constant ${constant.name} found which is not present in domain ${configEntry.domain}`));
          // quickfix will remove constant
        }
      }
 
      for (const d of domainValueInfo) {
        if (!def.constants.find(c => c.value === d.value)) {
          issues.push(
            Issue.atStatement(
              classContents,
              classContents.getStatements()[0],
              `Missing constant for ${d.value} (domain ${configEntry.domain})`,
              this.getMetadata().key,
              this.conf.severity));
          // quickfix will add constant
        }
      }
 
      if (configEntry.constantForDomainName && !domainNameConstantFound) {
        issues.push(
          Issue.atStatement(
            classContents,
            classContents.getStatements()[0],
            `Missing constant ${configEntry.constantForDomainName} for name of domain ${configEntry.domain}`,
            this.getMetadata().key,
            this.conf.severity));
      }
      return issues;
 
    }
 
    return [];
  }
 
  private issueAtConstant(constant: InfoConstant, message: string): Issue {
    return Issue.atIdentifier(
      constant.identifier,
      message,
      this.getMetadata().key,
      this.conf.severity);
  }
}