All files / src/abap cross_include_macros.ts

98.58% Statements 139/141
88.09% Branches 37/42
100% Functions 8/8
98.58% Lines 139/141

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 1421x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 14452x 14452x 1x 1x 14452x 14452x 14452x 14452x 14448x 14448x 4x 4x 4x 14452x 14452x 14452x 5x 5x     5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 4x 4x 4x 5x 4x 1x 1x 1x 1x 1x 1x 14452x 14452x 15377x 3x 3x 3x 15377x 14452x 1x 1x 13494x 13494x 1x 1x 1x 747x 747x 747x 750x 750x 2839x 2833x 2833x 2839x 2839x 2839x 3x 3x 3x 3x 3x 2839x 747x 747x 744x 744x 1x 1x 14452x 14452x 15377x 15327x 15327x 50x 50x 5x 5x 5x 50x 50x 14452x 14452x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 1x 1x 5x 5x 8x 5x 5x 4x 4x 5x 8x 1x 1x 1x 1x 1x  
import {IRegistry} from "../_iregistry";
import {ABAPObject} from "../objects/_abap_object";
import {Program} from "../objects/program";
import {Unknown} from "./2_statements/statements/_statement";
import {Include} from "./2_statements/statements";
import * as Expressions from "./2_statements/expressions";
import {ExpandMacros} from "./2_statements/expand_macros";
import {IncludeGraph} from "../utils/include_graph";
 
// Macros can be defined in one include and used in a sibling include, connected
// via the main program. Objects are parsed in isolation, so the sibling include
// initially ends up with Unknown statements. This second pass finds the main
// program(s) for such includes, collects the macro definitions visible there,
// and re-parses the include with the macro names as extra global macros.
export class CrossIncludeMacros {
  private readonly reg: IRegistry;
 
  public constructor(reg: IRegistry) {
    this.reg = reg;
  }
 
  public run(): void {
    this.reparseCyclicPrograms();
 
    const candidates = this.findCandidates();
    if (candidates.length === 0) {
      return;
    }
 
    const graph = new IncludeGraph(this.reg);
    const config = this.reg.getConfig();
    const globalMacros = config.getSyntaxSetttings().globalMacros || [];
 
    for (const prog of candidates) {
      const mains = this.findMains(graph, prog);
      if (mains.size === 0) {
        continue;
      }
 
      const expand = new ExpandMacros(globalMacros, config.getRelease(), this.reg, config.getLanguageVersion());
      for (const main of mains) {
        for (const file of main.getABAPFiles()) {
          // find() follows INCLUDE statements, so this collects macros from the full include chain
          expand.find([...file.getStatements()], file, false);
        }
      }
 
      const macroNames = expand.listMacroNames();
      if (this.matchesUnknown(prog, new Set(macroNames))) {
        prog.setDirty();
        prog.parse(config.getRelease(), macroNames, this.reg, config.getLanguageVersion());
      }
    }
  }
 
//////////////////////////////
 
  // A program or include in an INCLUDE cycle can be parsed while the include defining its macros is still
  // being parsed, depending on the order the files were added. Everything is parsed now, so parse it again.
  private reparseCyclicPrograms(): void {
    const config = this.reg.getConfig();
    for (const obj of this.reg.getObjects()) {
      if (obj instanceof Program && this.hasUnknown(obj) && this.isInIncludeCycle(obj)) {
        obj.setDirty();
        obj.parse(config.getRelease(), config.getSyntaxSetttings().globalMacros, this.reg, config.getLanguageVersion());
      }
    }
  }
 
  private hasUnknown(prog: Program): boolean {
    return prog.getABAPFiles().some(f => f.getStatements().some(s => s.get() instanceof Unknown));
  }
 
  // the include graph does not link INCLUDEs of main programs, so follow the INCLUDEs from the program
  private isInIncludeCycle(prog: Program): boolean {
    const seen = new Set<Program>();
    const stack = [prog];
    while (stack.length > 0) {
      for (const file of stack.pop()!.getABAPFiles()) {
        for (const statement of file.getStatements()) {
          if (!(statement.get() instanceof Include)) {
            continue;
          }
          const name = statement.findDirectExpression(Expressions.IncludeName)?.concatTokens();
          const included = this.reg.getObject("PROG", name);
          if (included === prog) {
            return true;
          } else if (included instanceof Program && seen.has(included) === false) {
            seen.add(included);
            stack.push(included);
          }
        }
      }
    }
    return false;
  }
 
  private findCandidates(): Program[] {
    const ret: Program[] = [];
    for (const obj of this.reg.getObjects()) {
      if (!(obj instanceof Program) || obj.isInclude() === false) {
        continue;
      }
      for (const file of obj.getABAPFiles()) {
        if (file.getStatements().some(s => s.get() instanceof Unknown)) {
          ret.push(obj);
          break;
        }
      }
    }
    return ret;
  }
 
  private findMains(graph: IncludeGraph, prog: Program): Set<ABAPObject> {
    const ret = new Set<ABAPObject>();
    const filename = prog.getMainABAPFile()?.getFilename();
    for (const mainFilename of graph.listMainForInclude(filename)) {
      const file = this.reg.getFileByName(mainFilename);
      const obj = file ? this.reg.findObjectForFile(file) : undefined;
      if (obj instanceof ABAPObject) {
        ret.add(obj);
      }
    }
    return ret;
  }
 
  private matchesUnknown(prog: Program, macroNames: Set<string>): boolean {
    for (const file of prog.getABAPFiles()) {
      for (const s of file.getStatements()) {
        if (s.get() instanceof Unknown) {
          const name = ExpandMacros.findName(s.getTokens());
          if (name !== undefined && macroNames.has(name.toUpperCase())) {
            return true;
          }
        }
      }
    }
    return false;
  }
 
}