All files / src/rules else_after_all_returns.ts

95.8% Statements 137/143
92.3% Branches 36/39
100% Functions 10/10
95.8% Lines 137/143

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 1x 1x 1x 1x 1x 1x 1x 14684x 14684x 14684x 14684x 114518x 114518x 114518x 114518x 114518x 114518x 114518x 114518x 114518x 114518x 114518x 14684x 14684x 14120x 14120x 14684x 14684x 264x 264x 14684x 14684x 311x 311x 311x 311x 12x 12x 299x 311x 22x 22x 6x 6x 16x 22x 10x 10x 10x     10x 10x 10x 10x 10x 10x 10x 10x 22x 299x 299x 299x 14684x 14684x 14684x 16x 16x 16x 5x 5x 11x 11x 16x 3x 3x 1x 1x 3x 10x 10x 10x 14684x 14684x 19x 1x 1x 18x 19x     18x 18x 14684x 14684x 38x 38x 40x 20x 20x 20x 20x 40x 38x 38x 14684x 14684x 18x 18x 18x 18x 18x 18x     14684x 14684x 18x 18x 18x 18x 11x 11x 18x 2x 2x 18x 2x 2x 2x 3x 3x 14684x 14684x  
import {Issue} from "../issue";
import {ABAPRule} from "./_abap_rule";
import {BasicRuleConfig} from "./_basic_rule_config";
import {IRuleMetadata, RuleTag} from "./_irule";
import {ABAPFile} from "../abap/abap_file";
import {ABAPObject} from "../objects/_abap_object";
import * as Statements from "../abap/2_statements/statements";
import * as Structures from "../abap/3_structures/structures";
import * as Expressions from "../abap/2_statements/expressions";
import {StatementNode, StructureNode} from "../abap/nodes";
import {EditHelper} from "../edit_helper";
import {Comment} from "../abap/2_statements/statements/_statement";
 
export class ElseAfterAllReturnsConf extends BasicRuleConfig {
}
 
export class ElseAfterAllReturns extends ABAPRule {
  private conf = new ElseAfterAllReturnsConf();
 
  public getMetadata(): IRuleMetadata {
    return {
      key: "else_after_all_returns",
      title: "Else after all returns",
      shortDescription: `Finds ELSE branches that are redundant because all prior branches exit unconditionally.`,
      extendedInformation: `When every branch preceding ELSE ends with RETURN, EXIT, or CONTINUE (or a non-resumable RAISE / LEAVE), ` +
        `the ELSE keyword adds no logical value and can be removed by de-indenting its body.`,
      tags: [RuleTag.SingleFile, RuleTag.Styleguide, RuleTag.Quickfix],
      badExample: `IF x = 1.\n  RETURN.\nELSE.\n  WRITE 'hello'.\nENDIF.`,
      goodExample: `IF x = 1.\n  RETURN.\nENDIF.\nWRITE 'hello'.`,
    };
  }
 
  public getConfig() {
    return this.conf;
  }
 
  public setConfig(conf: ElseAfterAllReturnsConf) {
    this.conf = conf;
  }
 
  public runParsed(file: ABAPFile, _obj: ABAPObject) {
    const issues: Issue[] = [];
 
    const structure = file.getStructure();
    if (structure === undefined) {
      return [];
    }
 
    for (const ifNode of structure.findAllStructures(Structures.If)) {
      const elseNode = ifNode.findDirectStructure(Structures.Else);
      if (elseNode === undefined) {
        continue;
      }
 
      if (this.allBranchesExit(ifNode)) {
        const elseStatement = elseNode.findFirstStatement(Statements.Else);
        const endifStatement = ifNode.findDirectStatement(Statements.EndIf);
        if (elseStatement === undefined || endifStatement === undefined) {
          continue;
        }
 
        const fix = EditHelper.merge(
          EditHelper.replaceRange(file, elseStatement.getFirstToken().getStart(), elseStatement.getLastToken().getEnd(), "ENDIF."),
          EditHelper.deleteStatement(file, endifStatement),
        );
        const message = "Redundant ELSE: all prior branches exit unconditionally";
        issues.push(Issue.atStatement(file, elseStatement, message, this.getMetadata().key, this.conf.severity, fix));
      }
    }
 
    return issues;
  }
 
  // Returns true if every branch before the ELSE ends with an unconditional exit.
  private allBranchesExit(ifNode: StructureNode): boolean {
    // Main IF body
    const mainBody = ifNode.findDirectStructure(Structures.Body);
    if (!this.bodyEndsWithExit(mainBody)) {
      return false;
    }
 
    // Each ELSEIF body
    for (const elseifNode of ifNode.findDirectStructures(Structures.ElseIf)) {
      const elseifBody = elseifNode.findDirectStructure(Structures.Body);
      if (!this.bodyEndsWithExit(elseifBody)) {
        return false;
      }
    }
 
    return true;
  }
 
  private bodyEndsWithExit(body: StructureNode | undefined): boolean {
    if (body === undefined) {
      return false;
    }
    const last = this.lastNonCommentStatement(body);
    if (last === undefined) {
      return false;
    }
    return this.isExit(last);
  }
 
  private collectStatements(node: StructureNode): StatementNode[] {
    const result: StatementNode[] = [];
    for (const child of node.getChildren()) {
      if (child instanceof StatementNode) {
        result.push(child);
      } else if (child instanceof StructureNode) {
        result.push(...this.collectStatements(child));
      }
    }
    return result;
  }
 
  private lastNonCommentStatement(node: StructureNode): StatementNode | undefined {
    const all = this.collectStatements(node);
    for (let i = all.length - 1; i >= 0; i--) {
      if (!(all[i].get() instanceof Comment)) {
        return all[i];
      }
    }
    return undefined;
  }
 
  private isExit(node: StatementNode): boolean {
    const s = node.get();
    if (s instanceof Statements.Return
        || s instanceof Statements.Exit
        || s instanceof Statements.Continue) {
      return true;
    }
    if (s instanceof Statements.Raise) {
      return !node.concatTokens().toUpperCase().includes("RESUMABLE");
    }
    if (s instanceof Statements.Leave) {
      const tokens = node.concatTokens().toUpperCase();
      return !tokens.includes("LIST-PROCESSING") && node.findFirstExpression(Expressions.AndReturn) === undefined;
    }
    return false;
  }
 
}