All files / src/rules unused_variables.ts

91.18% Statements 93/102
83.08% Branches 54/65
93.33% Functions 14/15
91.09% Lines 92/101

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 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 2131x 1x   1x   1x 1x 1x   1x   1x 1x   1x   1x       14619x     1x 7350x       21864x                                   7013x       114x 114x           198x 198x       220x 8x 212x 11x       201x 201x 5x     196x     196x 196x 93x 93x 32x 23x 23x     93x 23x   70x     196x       782x   782x       782x 587x     782x 586x     782x       587x   587x 587x 344x       344x       96x 248x       97x   97x 97x 3x 94x 1x     93x 93x       587x       94x       94x 94x 2x     92x 92x 579x 1x   578x 92x       91x       240x 240x 240x 19x   221x         97x 97x     97x 97x     97x 97x       97x 97x       93x 93x 2x     91x 91x 91x          
import {Issue} from "../issue";
import {BasicRuleConfig} from "./_basic_rule_config";
import {IRegistry} from "../_iregistry";
import {IRule, IRuleMetadata, RuleTag} from "./_irule";
import {IObject} from "../objects/_iobject";
import {SyntaxLogic} from "../abap/5_syntax/syntax";
import {ABAPObject} from "../objects/_abap_object";
import {ScopeType} from "../abap/5_syntax/_scope_type";
import {TypedIdentifier, IdentifierMeta} from "../abap/types/_typed_identifier";
import {Interface} from "../objects";
import {ISpaghettiScopeNode} from "../abap/5_syntax/_spaghetti_scope";
import {References} from "../lsp/references";
import {EditHelper, IEdit} from "../edit_helper";
import {StatementNode} from "../abap/nodes/statement_node";
import {Comment} from "../abap/2_statements/statements/_statement";
 
export class UnusedVariablesConf extends BasicRuleConfig {
  /** skip specific names, case insensitive
   * @uniqueItems true
  */
  public skipNames: string[] = [];
}
 
export class UnusedVariables implements IRule {
  private conf = new UnusedVariablesConf();
  private reg: IRegistry;
 
  public getMetadata(): IRuleMetadata {
    return {
      key: "unused_variables",
      title: "Unused variables",
      shortDescription: `Checks for unused variables and constants`,
      extendedInformation: `WARNING: slow
 
      Experimental, might give false positives. Skips event parameters.
 
      Note that this currently does not work if the source code uses macros.
 
      Unused variables are not reported if the object contains syntax errors.`,
      tags: [RuleTag.Quickfix],
      pragma: "##NEEDED",
      pseudoComment: "EC NEEDED",
    };
  }
 
  public getConfig() {
    return this.conf;
  }
 
  public setConfig(conf: UnusedVariablesConf) {
    this.conf = conf;
    Iif (this.conf.skipNames === undefined) {
      this.conf.skipNames = [];
    }
  }
 
  public initialize(reg: IRegistry) {
    this.reg = reg;
    return this;
  }
 
  public run(obj: IObject): Issue[] {
    if (!(obj instanceof ABAPObject)) {
      return [];
    } else if (obj instanceof Interface) { // todo, how to handle interfaces?
      return [];
    }
 
    // dont report unused variables when there are syntax errors
    const syntax = new SyntaxLogic(this.reg, obj).run();
    if (syntax.issues.length > 0) {
      return [];
    }
 
    const results = this.traverse(syntax.spaghetti.getTop(), obj);
 
    // remove duplicates, quick and dirty
    const deduplicated: Issue[] = [];
    for (const result of results) {
      let cont = false;
      for (const d of deduplicated) {
        if (result.getStart().equals(d.getStart())) {
          cont = true;
          break;
        }
      }
      if (cont === true) {
        continue;
      }
      deduplicated.push(result);
    }
 
    return deduplicated;
  }
 
  private traverse(node: ISpaghettiScopeNode, obj: ABAPObject): Issue[] {
    const ret: Issue[] = [];
 
    Iif (node.getIdentifier().stype === ScopeType.OpenSQL) {
      return [];
    }
 
    if (node.getIdentifier().stype !== ScopeType.BuiltIn) {
      ret.push(...this.checkNode(node, obj));
    }
 
    for (const c of node.getChildren()) {
      ret.push(...this.traverse(c, obj));
    }
 
    return ret;
  }
 
  private checkNode(node: ISpaghettiScopeNode, obj: ABAPObject): Issue[] {
    const ret: Issue[] = [];
 
    const vars = node.getData().vars;
    for (const name in vars) {
      Iif (this.conf.skipNames?.length > 0
          && this.conf.skipNames.some((a) => a.toUpperCase() === name)) {
        continue;
      }
      if (name === "ME"
          || name === "SUPER"
          || vars[name].getMeta().includes(IdentifierMeta.EventParameter)) {
        // todo, workaround for "me" and "super", these should somehow be typed to built-in
        continue;
      } else if ((obj.containsFile(vars[name].getFilename())
            || node.getIdentifier().stype === ScopeType.Program
            || node.getIdentifier().stype === ScopeType.Form)
          && this.isUsed(vars[name], node) === false) {
        const message = "Variable \"" + vars[name].getName() + "\" not used";
 
        const statement = this.findStatement(vars[name]);
        if (statement?.getPragmas().map(t => t.getStr()).includes(this.getMetadata().pragma + "")) {
          continue;
        } else if (this.suppressedbyPseudo(statement, vars[name], obj)) {
          continue;
        }
 
        const fix = this.buildFix(vars[name], obj);
        ret.push(Issue.atIdentifier(vars[name], message, this.getMetadata().key, this.conf.severity, fix));
      }
    }
 
    return ret;
  }
 
  private suppressedbyPseudo(statement: StatementNode | undefined, v: TypedIdentifier, obj: ABAPObject): boolean {
    Iif (statement === undefined) {
      return false;
    }
 
    const file = obj.getABAPFileByName(v.getFilename());
    if (file === undefined) {
      return false;
    }
 
    let next = false;
    for (const s of file.getStatements()) {
      if (next === true && s.get() instanceof Comment) {
        return s.concatTokens().includes(this.getMetadata().pseudoComment + "");
      }
      if (s === statement) {
        next = true;
      }
    }
 
    return false;
  }
 
  private isUsed(id: TypedIdentifier, node: ISpaghettiScopeNode): boolean {
    const isInline = id.getMeta().includes(IdentifierMeta.InlineDefinition);
    const found = new References(this.reg).search(id, node, true, isInline === false);
    if (isInline === true) {
      return found.length > 2; // inline definitions are always written to
    } else {
      return found.length > 1;
    }
  }
 
  private findStatement(v: TypedIdentifier): StatementNode | undefined {
    const file = this.reg.getFileByName(v.getFilename());
    Iif (file === undefined) {
      return undefined;
    }
    const object = this.reg.findObjectForFile(file);
    Iif (!(object instanceof ABAPObject)) {
      return undefined;
    }
    const abapfile = object.getABAPFileByName(v.getFilename());
    Iif (abapfile === undefined) {
      return undefined;
    }
 
    const statement = EditHelper.findStatement(v.getToken(), abapfile);
    return statement;
  }
 
  private buildFix(v: TypedIdentifier, obj: ABAPObject): IEdit | undefined {
    const file = obj.getABAPFileByName(v.getFilename());
    if (file === undefined) {
      return undefined;
    }
 
    const statement = EditHelper.findStatement(v.getToken(), file);
    Eif (statement) {
      return EditHelper.deleteStatement(file, statement);
    }
 
    return undefined;
  }
}