All files / src/rules constant_condition.ts

96.66% Statements 174/180
92.95% Branches 66/71
100% Functions 10/10
96.66% Lines 174/180

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 175 176 177 178 179 180 1811x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 14746x 14746x 14746x 14746x 14746x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 131194x 14746x 14746x 14120x 14120x 14746x 14746x 264x 264x 14746x 14746x 490x 490x 490x 14746x 14746x 587x 587x 115x 115x 472x 472x 587x 2523x 1x 1x 2522x 2523x 237x 119x 237x 117x 117x 237x 2522x 2522x 472x 472x 14746x 14746x 14746x 117x 117x 117x 117x 117x 117x 117x 117x 117x 38x 38x 79x 117x 117x 7x 7x 72x 117x 117x 4x 4x 68x 68x 68x 117x 117x 117x 117x 14746x 14746x 14746x 119x 119x 56x 56x 119x 45x 45x 45x 19x 19x 26x 26x 26x 45x 45x     26x 26x 26x 45x 45x 45x 45x 63x 14746x 14746x 78x 78x 5x 5x 78x 6x 78x 67x 67x 2x     2x 2x 67x 67x 1x 1x 66x 66x     14746x 14746x 102x 102x 102x 14746x 14746x  
import * as Expressions from "../abap/2_statements/expressions";
import {ExpressionNode} from "../abap/nodes";
import {CharacterType} from "../abap/types/basic";
import {Issue} from "../issue";
import {IObject} from "../objects/_iobject";
import {IRegistry} from "../_iregistry";
import {BasicRuleConfig} from "./_basic_rule_config";
import {IRule, IRuleMetadata, RuleTag} from "./_irule";
import {IFoldCandidate, IOccurrence, ObjectAnalysis, OccurrenceKind, PredicateKind, Procedure, Variable,
  characterConstant, deleteStatement, foldIf, foldIssues, initialDeclaration, mergeEdits, plainClear, plainMove,
  sameCharacters, simplePredicate} from "./_procedure_analysis";
 
export class ConstantConditionConf extends BasicRuleConfig {
}
 
export class ConstantCondition implements IRule {
  private reg: IRegistry;
  private conf = new ConstantConditionConf();
 
  public getMetadata(): IRuleMetadata {
    return {
      key: "constant_condition",
      title: "Constant condition",
      shortDescription: `Finds IF and ELSEIF conditions on local variables whose value is known`,
      extendedInformation: `Reports two cases, for local variables of a METHOD, FORM or FUNCTION.
 
1. DATA(c) = <constant>. where the constant is a character literal like 'X', or a character constant like
abap_true, abap_false or space, and the only other reference to c is the condition of an IF or ELSEIF:
c = <constant>, c <> <constant>, EQ, NE, c IS INITIAL, c IS NOT INITIAL, optionally with NOT.
The declaration must be executed before the condition on every path: the IF is in the block of the
declaration or a nested block. The quick fix deletes the declaration.
 
2. DATA v TYPE ... without VALUE, or DATA(v) = VALUE t( ). with an explicit type and nothing in the parentheses,
where v is never changed, except by CLEAR v., and its address is never taken, in a condition v IS [NOT] INITIAL
or v IS [NOT] BOUND. The declaration is kept.
 
The quick fix removes the IF branch that is never taken, or keeps the branch that is always taken and
removes the rest, also in ELSEIF chains: a first IF branch that is never taken turns the next ELSEIF into IF,
an ELSEIF branch that is always taken turns into ELSE. Nothing is reported if a removed branch declares
data that is used outside of it.
 
Objects with syntax errors, macros or unknown statements are skipped, as are procedures with dynamic code,
INCLUDE, test seams, enhancements or native SQL. No release requirement.`,
      tags: [RuleTag.Quickfix],
      badExample: `DATA(lv_active) = abap_true.
IF lv_active = abap_true.
  run( ).
ELSE.
  skip( ).
ENDIF.`,
      goodExample: `run( ).`,
    };
  }
 
  public getConfig() {
    return this.conf;
  }
 
  public setConfig(conf: ConstantConditionConf) {
    this.conf = conf;
  }
 
  public initialize(reg: IRegistry) {
    this.reg = reg;
    return this;
  }
 
  public run(obj: IObject): readonly Issue[] {
    const analysis = ObjectAnalysis.run(this.reg, obj);
    if (analysis === undefined) {
      return [];
    }
 
    const issues: Issue[] = [];
    for (const procedure of analysis.procedures) {
      if (procedure.supported === false) {
        continue;
      }
      const candidates: IFoldCandidate[] = [];
      for (const variable of procedure.locals()) {
        if (initialDeclaration(variable) !== undefined) {
          this.initialVariable(procedure, variable, candidates);
        } else if (variable.inline) {
          this.constantVariable(procedure, variable, candidates);
        }
      }
      issues.push(...foldIssues(procedure.file, candidates, this.getMetadata().key, this.conf.severity));
    }
    return issues;
  }
 
  /** DATA(c) = abap_true. IF c = abap_true. */
  private constantVariable(procedure: Procedure, variable: Variable, candidates: IFoldCandidate[]) {
    const declaration = variable.declaration!;
    const move = plainMove(declaration);
    const read = variable.occurrences[0];
    if (move === undefined || move.operator !== "="
        || move.target.getChildren().length !== 1
        || move.target.findDirectExpression(Expressions.InlineData) === undefined
        || !(variable.identifier.getType() instanceof CharacterType)
        || variable.occurrences.length !== 1 || read.kind !== OccurrenceKind.Read
        || !procedure.dominates(declaration, read.statement)) {
      return;
    }
    const value = characterConstant(procedure, move.source);
    const result = value === undefined ? undefined : this.evaluate(procedure, read, value);
    if (result === undefined) {
      return;
    }
    const structure = procedure.ifStructure(read.statement);
    const fold = structure === undefined ? undefined : foldIf(procedure, structure, read.statement, result);
    if (structure === undefined || fold === undefined) {
      return;
    }
    candidates.push({
      structure,
      statement: read.statement,
      message: "Condition is always " + (result ? "true" : "false") + ", " + variable.identifier.getName() + " is constant",
      fix: mergeEdits(procedure.file, [fold, deleteStatement(procedure.file, declaration)]),
    });
  }
 
  /** DATA v TYPE ... or DATA(v) = VALUE t( ). never changed: IF v IS INITIAL / IS BOUND */
  private initialVariable(procedure: Procedure, variable: Variable, candidates: IFoldCandidate[]) {
    if (variable.escapes()
        || variable.writes().some(w => plainClear(w.statement)?.getStart().equals(w.token.getStart()) !== true)) {
      return;
    }
    for (const read of variable.reads()) {
      const predicate = simplePredicate(read.statement);
      if (predicate === undefined || !this.isOperand(predicate.left, read)
          || (predicate.kind !== PredicateKind.Initial && predicate.kind !== PredicateKind.Bound)) {
        continue;
      }
      // initial: IS INITIAL is true, IS BOUND is false
      const result = (predicate.kind === PredicateKind.Initial) !== predicate.negated;
      const structure = procedure.ifStructure(read.statement);
      const fix = structure === undefined ? undefined : foldIf(procedure, structure, read.statement, result);
      if (structure === undefined || fix === undefined) {
        continue;
      }
      candidates.push({
        structure,
        statement: read.statement,
        message: "Condition is always " + (result ? "true" : "false") + ", " + variable.identifier.getName() + " is never changed",
        fix,
      });
    }
  }
 
  private evaluate(procedure: Procedure, read: IOccurrence, value: string): boolean | undefined {
    const predicate = simplePredicate(read.statement);
    if (predicate === undefined) {
      return undefined;
    }
    if (predicate.kind === PredicateKind.Initial && this.isOperand(predicate.left, read)) {
      return sameCharacters(value, "") !== predicate.negated;
    } else if (predicate.kind === PredicateKind.Equal) {
      let other = predicate.right!;
      if (!this.isOperand(predicate.left, read)) {
        if (!this.isOperand(predicate.right!, read)) {
          return undefined;
        }
        other = predicate.left;
      }
      const compared = characterConstant(procedure, other);
      if (compared === undefined) {
        return undefined;
      }
      return sameCharacters(value, compared) !== predicate.negated;
    }
    return undefined;
  }
 
  private isOperand(source: ExpressionNode, read: IOccurrence): boolean {
    return source.countTokens() === 1 && source.findDirectExpression(Expressions.FieldChain) !== undefined
      && source.getFirstToken().getStart().equals(read.token.getStart());
  }
 
}