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 11818x 11818x 11818x 11818x 57877x 57877x 57877x 57877x 57877x 57877x 57877x 57877x 57877x 57877x 57877x 11818x 11818x 11279x 11279x 11818x 11818x 253x 253x 11818x 11818x 291x 291x 291x 291x 12x 12x 279x 291x 21x 21x 5x 5x 16x 21x 10x 10x 10x     10x 10x 10x 10x 10x 10x 10x 10x 21x 279x 279x 279x 11818x 11818x 11818x 16x 16x 16x 5x 5x 11x 11x 16x 3x 3x 1x 1x 3x 10x 10x 10x 11818x 11818x 19x 1x 1x 18x 19x     18x 18x 11818x 11818x 38x 38x 40x 20x 20x 20x 20x 40x 38x 38x 11818x 11818x 18x 18x 18x 18x 18x 18x     11818x 11818x 18x 18x 18x 18x 11x 11x 18x 2x 2x 18x 2x 2x 2x 3x 3x 11818x 11818x  
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),
        );
        // eslint-disable-next-line max-len
        issues.push(Issue.atStatement(file, elseStatement, "Redundant ELSE: all prior branches exit unconditionally", 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;
  }
 
}