All files / src/rules abapdoc.ts

90.97% Statements 131/144
88.37% Branches 38/43
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 29370x 29370x 29370x 29370x 29370x 29370x 29370x 29370x 1x 14686x 14686x 14686x 14686x 14686x 72189x 72189x 72189x 72189x 72189x 72189x 72189x 72189x 72189x 72189x 72189x 72189x 14686x 14686x 14120x 14120x 14686x 14686x 267x 267x 14686x 14686x 322x 322x 322x 322x 322x 322x 322x 322x 151x 86x 86x 151x 4x 4x 61x 61x 151x 2x 2x 2x 2x 2x 2x 2x 151x 322x 322x 69x 39x 39x 30x 30x 69x               69x 322x 322x 53x 1x 1x 52x 53x     52x 53x 63x 7x 7x 7x 7x 63x 33x 33x 33x 33x 63x         63x 63x 52x 52x 322x 322x 14686x 14686x 52x 52x 52x 52x 52x 7x 7x 7x 52x 52x 52x 52x 19x 30x 30x 30x 19x 19x 11x 11x 19x 52x 52x 14686x
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();
          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();
          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;
  }
}