All files / src/rules changing_sorted_line.ts

94.36% Statements 201/213
77.77% Branches 63/81
100% Functions 10/10
94.36% Lines 201/213

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 213 2141x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 14682x 14682x 14682x 14682x 14682x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 43916x 14682x 14682x 14120x 14120x 14682x 14682x 285x 285x 285x 14682x 14682x 264x 264x 14682x 14682x 382x 100x 100x 282x 282x 282x 282x 382x 292x 292x 273x 273x 19x 19x 19x 19x 19x 19x 19x 292x 74x 74x 74x 231x 19x 19x 231x 231x 74x 19x 282x 282x 282x 14682x 14682x 14682x 14682x 19x 19x 38x 38x 18x 18x 18x 18x 2x 2x 16x 18x 18x 8x 8x 18x 18x 18x 18x 18x 18x 38x 38x 19x 14682x 14682x 14682x 231x 231x 231x 231x 231x 210x 210x 21x 21x 21x 21x 231x 6x 231x 10x 15x 2x 5x 3x 3x 3x 21x 231x 231x 231x 15x 231x 6x 6x 231x 14682x 14682x 14682x 14682x 20x 20x 18x 18x     18x 18x 20x 20x 20x 2x 1x 1x 2x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x             19x     19x 19x 19x 14682x 14682x 35x 35x 197x 35x 35x 197x     14682x 14682x  
import * as Expressions from "../abap/2_statements/expressions";
import * as Statements from "../abap/2_statements/statements";
import * as Structures from "../abap/3_structures/structures";
import {BasicRuleConfig} from "./_basic_rule_config";
import {Issue} from "../issue";
import {IRule, IRuleMetadata, RuleTag} from "./_irule";
import {SyntaxLogic} from "../abap/5_syntax/syntax";
import {IObject} from "../objects/_iobject";
import {ABAPObject} from "../objects/_abap_object";
import {IRegistry} from "../_iregistry";
import {StructureType, TableAccessType, TableType} from "../abap/types/basic";
import {AbstractType} from "../abap/types/basic/_abstract_type";
import {ExpressionNode, StatementNode} from "../abap/nodes";
import {ABAPFile} from "../abap/abap_file";
import {ISpaghettiScope} from "../abap/5_syntax/_spaghetti_scope";
import {TypedIdentifier} from "../abap/types/_typed_identifier";
import {Dash, InstanceArrow, StaticArrow} from "../abap/1_lexer/tokens";
import {Position} from "../position";
import {INode} from "../abap/nodes/_inode";
 
export class ChangingSortedLineConf extends BasicRuleConfig {
}
 
export class ChangingSortedLine implements IRule {
  private reg: IRegistry;
  private conf = new ChangingSortedLineConf();
 
  public getMetadata(): IRuleMetadata {
    return {
      key: "changing_sorted_line",
      title: "Line of a sorted or hashed table passed to CHANGING",
      shortDescription: `A field symbol on a whole line of a sorted or hashed table, passed to a CHANGING parameter of a method`,
      extendedInformation: `The key fields of a sorted or hashed table line are write-protected, so the line as a whole is
write-protected. Binding it to a CHANGING parameter activates, but the call fails at runtime with
CX_SY_DYN_CALL_ILLEGAL_TYPE, "the actual parameter for ... is write-protected", even when the method does not
change the key.
 
Only field symbols assigned in the same method, form or function module are checked, via READ TABLE ... ASSIGNING,
LOOP AT ... ASSIGNING, INSERT ... INTO TABLE ... ASSIGNING and ASSIGN itab[ ... ] TO.
 
Fix: read the line into a work area, pass the work area and MODIFY TABLE ... FROM it afterwards,
or pass only the non-key fields the method needs.`,
      tags: [RuleTag.SingleFile],
      badExample: `READ TABLE lt_sum WITH TABLE KEY app = lv_app ASSIGNING FIELD-SYMBOL(<sum>).
add( CHANGING cs_sum = <sum> ).`,
      goodExample: `READ TABLE lt_sum WITH TABLE KEY app = lv_app INTO DATA(ls_sum).
add( CHANGING cs_sum = ls_sum ).
MODIFY TABLE lt_sum FROM ls_sum.`,
    };
  }
 
  public getConfig() {
    return this.conf;
  }
 
  public initialize(reg: IRegistry) {
    this.reg = reg;
    return this;
  }
 
  public setConfig(conf: ChangingSortedLineConf): void {
    this.conf = conf;
  }
 
  public run(obj: IObject): Issue[] {
    if (!(obj instanceof ABAPObject) || obj.getType() === "INTF") {
      return [];
    }
 
    const issues: Issue[] = [];
    let spaghetti: ISpaghettiScope | undefined = undefined;
 
    for (const file of obj.getABAPFiles()) {
      const stru = file.getStructure();
      if (stru === undefined || stru.findFirstExpression(Expressions.MethodParameters) === undefined) {
        continue;
      }
      if (spaghetti === undefined) {
        spaghetti = new SyntaxLogic(this.reg, obj).run().spaghetti;
      }
 
      const blocks = [...stru.findAllStructures(Structures.Method),
        ...stru.findAllStructures(Structures.Form),
        ...stru.findAllStructures(Structures.FunctionModule)];
      for (const block of blocks) {
        // field symbol name -> name of the sorted or hashed table it points into
        const lines = new Map<string, string>();
        for (const statement of block.findAllStatementNodes()) {
          for (const p of statement.findAllExpressions(Expressions.MethodParameters)) {
            this.checkChanging(p, lines, spaghetti, file, issues);
          }
          this.updateAssigned(statement, lines, spaghetti, file);
        }
      }
    }
 
    return issues;
  }
 
////////////////
 
  private checkChanging(params: ExpressionNode, lines: Map<string, string>, spaghetti: ISpaghettiScope, file: ABAPFile, issues: Issue[]) {
    let changing = false;
    for (const child of params.getChildren()) {
      const str = child.concatTokens().toUpperCase();
      if (child instanceof ExpressionNode && changing === true && child.get() instanceof Expressions.ParameterListT) {
        for (const p of child.findDirectExpressions(Expressions.ParameterT)) {
          const target = p.findDirectExpression(Expressions.Target);
          const fs = target?.getChildren().length === 1 ? target.findDirectExpression(Expressions.TargetFieldSymbol) : undefined;
          if (fs === undefined) {
            continue;
          }
          const table = lines.get(fs.concatTokens().toUpperCase());
          const type = this.findReference(fs.getFirstToken().getStart(), spaghetti, file)?.getType();
          if (table === undefined || type === undefined || type.isGeneric()) {
            continue;
          }
          const name = p.findDirectExpression(Expressions.ParameterName)?.concatTokens();
          const message = `Line of sorted or hashed table ${table} passed to CHANGING ${name} is write-protected, ` +
            `the call fails at runtime. Use a work area and MODIFY TABLE ... FROM, or pass only non-key fields`;
          issues.push(Issue.atToken(file, fs.getFirstToken(), message, this.getMetadata().key, this.conf.severity));
        }
      }
      changing = str === "CHANGING";
    }
  }
 
  /** Tracks which field symbols the last assignment pointed to a whole line of a sorted or hashed table */
  private updateAssigned(statement: StatementNode, lines: Map<string, string>, spaghetti: ISpaghettiScope, file: ABAPFile) {
    const get = statement.get();
    const fs = get instanceof Statements.Unassign
      ? statement.findDirectExpression(Expressions.TargetFieldSymbol)
      : statement.findFirstExpression(Expressions.FSTarget)?.findFirstExpression(Expressions.TargetFieldSymbol);
    if (fs === undefined) {
      return;
    }
    const name = fs.concatTokens().toUpperCase();
 
    let table: INode | undefined = undefined;
    let isTableExpression = false;
    if (get instanceof Statements.ReadTable) {
      table = statement.findDirectExpression(Expressions.SimpleSource2) ?? statement.findDirectExpression(Expressions.Source);
    } else if (get instanceof Statements.Loop) {
      table = statement.findDirectExpression(Expressions.LoopSource)?.getFirstChild();
    } else if (get instanceof Statements.InsertInternal && statement.findDirectTokenByText("TABLE") !== undefined) {
      table = statement.findDirectExpression(Expressions.Target);
    } else if (get instanceof Statements.Assign) {
      table = statement.findDirectExpression(Expressions.AssignSource)?.getFirstChild();
      isTableExpression = true;
    }
 
    const tableType = table instanceof ExpressionNode ? this.findTableType(table, isTableExpression, spaghetti, file) : undefined;
    if (table instanceof ExpressionNode && tableType instanceof TableType
        && (tableType.getAccessType() === TableAccessType.sorted || tableType.getAccessType() === TableAccessType.hashed)) {
      lines.set(name, table.concatTokens().replace(/\[.*$/, "").trim());
    } else {
      lines.delete(name);
    }
  }
 
  /** The type of a table given by a plain field chain of names, components and attributes, else undefined.
   *  With tableExpression, the chain must end with a table expression, and the type of the chain before it is returned */
  private findTableType(node: ExpressionNode, tableExpression: boolean,
                        spaghetti: ISpaghettiScope, file: ABAPFile): AbstractType | undefined {
    if (!(node.get() instanceof Expressions.Target)) {
      const chain = node.getChildren().length === 1 ? node.findDirectExpression(Expressions.FieldChain) : undefined;
      if (chain === undefined) {
        return undefined;
      }
      node = chain;
    }
 
    const children = [...node.getChildren()];
    if (tableExpression === true) {
      if (!(children.pop()?.get() instanceof Expressions.TableExpression)) {
        return undefined;
      }
    }
 
    let type: AbstractType | undefined = undefined;
    for (const child of children) {
      const get = child.get();
      if (get instanceof Expressions.SourceField
          || get instanceof Expressions.SourceFieldSymbol
          || get instanceof Expressions.TargetField
          || get instanceof Expressions.TargetFieldSymbol
          || get instanceof Expressions.AttributeName) {
        type = this.findReference(child.getFirstToken().getStart(), spaghetti, file)?.getType();
      } else if (get instanceof Expressions.ComponentName) {
        type = type instanceof StructureType ? type.getComponentByName(child.concatTokens()) : undefined;
      } else if (get instanceof Expressions.ClassName || get instanceof StaticArrow) {
        continue;
      } else if (!(get instanceof Dash) && !(get instanceof InstanceArrow)) {
        return undefined;
      }
      if (type === undefined) {
        return undefined;
      }
    }
    return type;
  }
 
  private findReference(start: Position, spaghetti: ISpaghettiScope, file: ABAPFile) {
    const scope = spaghetti.lookupPosition(start, file.getFilename());
    for (const r of scope?.getData().references || []) {
      if (r.position.getStart().equals(start) && r.resolved instanceof TypedIdentifier) {
        return r.resolved;
      }
    }
    return undefined;
  }
 
}