All files / src/rules abapdoc.ts

90.97% Statements 131/144
89.47% Branches 34/38
100% Functions 7/7
90.97% Lines 131/144

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 1441x 1x 1x 1x 1x 1x 1x 1x 1x 21011x 21011x 21011x 21011x 21011x 21011x 21011x 21011x 1x 10506x 10506x 10506x 10506x 10506x 31366x 31366x 31366x 31366x 31366x 31366x 31366x 31366x 31366x 31366x 31366x 31366x 10506x 10506x 9970x 9970x 10506x 10506x 254x 254x 10506x 10506x 289x 289x 289x 289x 289x 289x 289x 289x 132x 71x 71x 132x 4x 4x 57x 57x 132x 2x 2x 2x 2x 2x 2x 2x 132x 289x 289x 64x 36x 36x 28x 28x 64x               64x 289x 289x 49x 1x 1x 48x 49x     48x 49x 59x 7x 7x 7x 7x 59x 30x 30x 30x 30x 59x         59x 59x 48x 48x 289x 289x 10506x 10506x 48x 48x 48x 48x 48x 7x 7x 7x 48x 48x 48x 48x 18x 29x 29x 29x 18x 18x 11x 11x 18x 48x 48x 10506x
import {ABAPRule} from "./_abap_rule";
import {BasicRuleConfig} from "./_basic_rule_config";
import {Issue} from "../issue";
import {Visibility} from "../abap/4_file_information/visibility";
import {InfoMethodDefinition} from "../abap/4_file_information/_abap_file_information";
import {IRuleMetadata, RuleTag} from "./_irule";
import {ABAPFile} from "../abap/abap_file";
import {Position} from "../position";
 
export class AbapdocConf extends BasicRuleConfig {
  /** Check local classes and interfaces for abapdoc. */
  public checkLocal: boolean = false;
  public classDefinition: boolean = false;
  public interfaceDefinition: boolean = false;
  /** Ignores classes flagged as FOR TESTING */
  public ignoreTestClasses: boolean = true;
}
 
export class Abapdoc extends ABAPRule {
 
  private conf = new AbapdocConf();
 
  public getMetadata(): IRuleMetadata {
    return {
      key: "abapdoc",
      title: "Check abapdoc",
      shortDescription: `Various checks regarding abapdoc.`,
      extendedInformation: `Base rule checks for existence of abapdoc for public class methods and all interface methods.
 
Plus class and interface definitions.
 
https://github.com/SAP/styleguides/blob/main/clean-abap/CleanABAP.md#abap-doc-only-for-public-apis`,
      tags: [RuleTag.SingleFile, RuleTag.Styleguide],
    };
  }
 
  public getConfig() {
    return this.conf;
  }
 
  public setConfig(conf: AbapdocConf): void {
    this.conf = conf;
  }
 
  public runParsed(file: ABAPFile) {
    const issues: Issue[] = [];
    const rows = file.getRawRows();
    const regexEmptyTags = '^\\"! .*<[^/>]*><\\/';
    const regexEmptyAbapdoc = '^\\"!.+$';
    const regexEmptyParameterName = '^\\"! @parameter .+\\|';
 
    let methods: InfoMethodDefinition[] = [];
    for (const classDef of file.getInfo().listClassDefinitions()) {
      if (this.conf.checkLocal === false && classDef.isLocal === true) {
        continue;
      }
      if (this.conf.ignoreTestClasses === true && classDef.isForTesting === true) {
        continue;
      }
      methods = methods.concat(classDef.methods.filter(m => m.visibility === Visibility.Public));
 
      if (this.conf.classDefinition === true) {
        const previousRow = classDef.identifier.getStart().getRow() - 2;
        if (rows[previousRow]?.trim().substring(0, 2) !== "\"!") {
          const message = "Missing ABAP Doc for class " + classDef.identifier.getToken().getStr();
          const issue = Issue.atIdentifier(classDef.identifier, message, this.getMetadata().key, this.conf.severity);
          issues.push(issue);
        }
      }
    }
 
    for (const interfaceDef of file.getInfo().listInterfaceDefinitions()) {
      if (this.conf.checkLocal === false && interfaceDef.isLocal === true) {
        continue;
      }
      methods = methods.concat(interfaceDef.methods);
 
      if (this.conf.interfaceDefinition === true) {
        const previousRow = interfaceDef.identifier.getStart().getRow() - 2;
        if (rows[previousRow]?.trim().substring(0, 2) !== "\"!") {
          const message = "Missing ABAP Doc for interface " + interfaceDef.identifier.getToken().getStr();
          const issue = Issue.atIdentifier(interfaceDef.identifier, message, this.getMetadata().key, this.conf.severity);
          issues.push(issue);
        }
      }
    }
 
    for (const method of methods) {
      if (method.isRedefinition === true) {
        continue;
      }
      const previousRowsTexts = this.getAbapdoc(rows, method.identifier.getStart());
      if (previousRowsTexts === undefined) {
        continue;
      }
 
      for (const rowText of previousRowsTexts) {
        if (rowText.trim().match(regexEmptyTags) !== null) {
          const message = "Empty tag(s) in ABAP Doc for method " + method.identifier.getToken().getStr() + " (" + rowText + ")";
          const issue = Issue.atIdentifier(method.identifier, message, this.getMetadata().key, this.conf.severity);
          issues.push(issue);
        }
        if (rowText.trim().match(regexEmptyAbapdoc) === null && previousRowsTexts.indexOf(rowText) === previousRowsTexts.length - 1) {
          const message = "Missing ABAP Doc for method " + method.identifier.getToken().getStr() + " (" + rowText + ")";
          const issue = Issue.atIdentifier(method.identifier, message, this.getMetadata().key, this.conf.severity);
          issues.push(issue);
        }
        if (rowText.trim().match(regexEmptyParameterName) !== null) {
          const message = "Missing ABAP Doc parameter name for method " + method.identifier.getToken().getStr() + " (" + rowText + ")";
          const issue = Issue.atIdentifier(method.identifier, message, this.getMetadata().key, this.conf.severity);
          issues.push(issue);
        }
 
      }
 
    }
    return issues;
  }
 
  private getAbapdoc(rows: readonly string[], pos: Position): string[] {
    let previousRow = pos.getRow() - 2;
    let rowText = rows[previousRow].trim().toUpperCase();
    const text: string[] = [];
 
    if (rowText === "METHODS" || rowText === "CLASS-METHODS") {
      previousRow--;
      rowText = rows[previousRow].trim().toUpperCase();
    }
    text.push(rowText);
    //we need to push the first row despite if it is actually an abapdoc or not
    //if the first row above a method is abapdoc then try to get the rest of the abapdoc block above
    if (rowText.trim().substring(0, 2) === "\"!") {
      while (previousRow >= 0) {
        previousRow--;
        rowText = rows[previousRow].trim().toUpperCase();
        if (rowText.trim().substring(0, 2) !== "\"!") {
          break;
        }
        text.push(rowText);
      }
    }
    return text;
  }
}