├── .npmrc ├── abapmerge ├── .gitignore ├── src ├── index.ts ├── abapmerge_marker.ts ├── collect_statements.ts ├── class.ts ├── utils.ts ├── graph.ts ├── file.ts ├── file_list.ts ├── interface_parser.ts ├── web.ts ├── class_list.ts ├── cli.ts ├── merge.ts ├── class_parser.ts └── pragma.ts ├── .npmignore ├── sample ├── subpkg │ ├── zif_app.intf.abap │ ├── zcx_error.clas.abap │ └── zcl_app.clas.abap └── ztest.prog.abap ├── .github ├── dependabot.yml └── workflows │ └── nodejs.yml ├── test ├── abapmerge_marker.ts ├── graph.ts ├── locals.ts ├── skip_test_classes.ts ├── includes.ts ├── cli.ts ├── pragmas.ts ├── interface_parser.ts ├── class_list.ts ├── test.ts └── class_parser.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── eslint.config.mjs /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true -------------------------------------------------------------------------------- /abapmerge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("./build/src/index"); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | coverage 4 | .nyc_output 5 | .vscode/ 6 | /*.abap 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Logic } from "./cli"; 2 | 3 | process.exit(Logic.run(process.argv)); 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | *.js.map 3 | .github 4 | test 5 | sample 6 | tsconfig.json 7 | /src/* 8 | .eslintrc.js -------------------------------------------------------------------------------- /sample/subpkg/zif_app.intf.abap: -------------------------------------------------------------------------------- 1 | interface zif_app 2 | public . 3 | 4 | methods run raising zcx_error. 5 | 6 | endinterface. 7 | -------------------------------------------------------------------------------- /sample/ztest.prog.abap: -------------------------------------------------------------------------------- 1 | report ztest. 2 | 3 | data gi_app type zif_app. 4 | create object gi_app type zcl_app. 5 | gi_app->run( ). 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | versioning-strategy: increase 10 | -------------------------------------------------------------------------------- /sample/subpkg/zcx_error.clas.abap: -------------------------------------------------------------------------------- 1 | class zcx_error definition 2 | public 3 | inheriting from cx_static_check 4 | final 5 | create public . 6 | 7 | public section. 8 | protected section. 9 | private section. 10 | endclass. 11 | 12 | 13 | 14 | class zcx_error implementation. 15 | endclass. 16 | -------------------------------------------------------------------------------- /sample/subpkg/zcl_app.clas.abap: -------------------------------------------------------------------------------- 1 | class zcl_app definition 2 | public 3 | create public . 4 | 5 | public section. 6 | interfaces zif_app. 7 | protected section. 8 | private section. 9 | endclass. 10 | 11 | class zcl_abapgit_zip implementation. 12 | method zif_app~run. 13 | write: / 'Hello'. 14 | endmethod. 15 | endclass. 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Use Node.js 11 | uses: actions/setup-node@v4 12 | - run: npm install 13 | - run: npm test 14 | env: 15 | CI: true 16 | -------------------------------------------------------------------------------- /test/abapmerge_marker.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import AbapmergeMarker from "../src/abapmerge_marker"; 3 | 4 | describe("Abapmerge marker", () => { 5 | it("Abapmerge marker renders", () => { 6 | const marker = new AbapmergeMarker().render(); 7 | expect(marker).to.match(/\* abapmerge (?:(\d+\.[.\d]*\d+))/); 8 | expect(marker).to.match(/^INTERFACE lif_abapmerge_marker\.$/m); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "build", 5 | "noFallthroughCasesInSwitch": true, 6 | "noImplicitReturns": true, 7 | "noUnusedParameters": true, 8 | "noUnusedLocals": true, 9 | "sourceMap": true, 10 | "downlevelIteration": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "lib": ["es2020", "dom"] 14 | }, 15 | "filesGlob": [ 16 | "./**/*.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "build", 21 | "typings" 22 | ] 23 | } -------------------------------------------------------------------------------- /src/abapmerge_marker.ts: -------------------------------------------------------------------------------- 1 | import PackageInfo from "../package.json"; 2 | 3 | export default class AbapmergeMarker { 4 | public render(): string { 5 | const timestamp = new Date().toJSON(); 6 | const abapmergeVersion = PackageInfo.version; 7 | 8 | return ` 9 | **************************************************** 10 | INTERFACE lif_abapmerge_marker. 11 | * abapmerge ${ abapmergeVersion } - ${ timestamp } 12 | CONSTANTS c_merge_timestamp TYPE string VALUE \`${ timestamp }\`. 13 | CONSTANTS c_abapmerge_version TYPE string VALUE \`${ abapmergeVersion }\`. 14 | ENDINTERFACE. 15 | **************************************************** 16 | `; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/collect_statements.ts: -------------------------------------------------------------------------------- 1 | /** collect statements split over multiple lines into single lines */ 2 | export class CollectStatements { 3 | 4 | public static collect(code: string): string { 5 | const output: string[] = []; 6 | 7 | const lines = code.split("\n"); 8 | while (lines.length > 0) { 9 | const line = lines.shift(); 10 | if (line.trimStart().toUpperCase().startsWith("REPORT ") === false 11 | && line.trimStart().toUpperCase().startsWith("INCLUDE ") === false 12 | && line.trimStart().toUpperCase().startsWith("INCLUDE:") === false) { 13 | output.push(line); 14 | continue; 15 | } 16 | 17 | let add = line; 18 | while (add.includes(".") === false) { 19 | add += " " + lines.shift(); 20 | } 21 | output.push(add); 22 | } 23 | 24 | return output.join("\n"); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /test/graph.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import Graph from "../src/graph"; 3 | 4 | const expect = chai.expect; 5 | 6 | describe("graph 1, test", () => { 7 | it("something", () => { 8 | const g = new Graph(); 9 | 10 | g.addNode("node1", "value1"); 11 | g.addNode("node2", "value2"); 12 | 13 | g.addEdge("node1", "node2"); 14 | 15 | expect(g.popLeaf()).to.equal("value2"); 16 | expect(g.popLeaf()).to.equal("value1"); 17 | }); 18 | }); 19 | 20 | 21 | describe("graph 2, test", () => { 22 | it("something", () => { 23 | const g = new Graph(); 24 | 25 | g.addNode("node1", "value1"); 26 | g.addNode("node2", "value2"); 27 | g.addNode("node3", "value3"); 28 | 29 | g.addEdge("node1", "node3"); 30 | g.addEdge("node2", "node3"); 31 | 32 | expect(g.popLeaf()).to.equal("value3"); 33 | 34 | expect(g.countNodes()).to.equal(2); 35 | expect(g.countEdges()).to.equal(0); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/class.ts: -------------------------------------------------------------------------------- 1 | export default class Class { 2 | 3 | private name: string; 4 | private definition: string; 5 | private implementation: string; 6 | private forTesting: boolean; 7 | private dependencies: string[]; 8 | 9 | public constructor(name: string, definition: string, forTesting: boolean, implementation?: string, dependencies?: string[]) { 10 | this.name = name; 11 | this.definition = definition; 12 | this.implementation = implementation; 13 | this.dependencies = dependencies; 14 | this.forTesting = forTesting; 15 | } 16 | 17 | public getName(): string { 18 | return this.name; 19 | } 20 | 21 | public getDefinition(): string { 22 | return this.definition; 23 | } 24 | 25 | public getImplementation(): string { 26 | return this.implementation; 27 | } 28 | 29 | public getDependencies(): string[] { 30 | return this.dependencies; 31 | } 32 | 33 | public isForTesting(): boolean { 34 | return this.forTesting; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Lars Hvam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export default class Utils { 2 | 3 | public static minstdRandMax = 2147483646; 4 | 5 | public static nextMinstdRandFor(seed: number): number { 6 | const product = 48271 * seed; 7 | return product % (Utils.minstdRandMax + 1); 8 | } 9 | 10 | public static hashCodeOf(input: string): number { 11 | let hash = 0; 12 | 13 | if (input.length === 0) { 14 | return hash; 15 | } 16 | 17 | for (let i = 0; i < input.length; ++i) { 18 | const char = input.charCodeAt(i); 19 | /* eslint-disable-next-line no-bitwise */ 20 | hash = ( ( hash << 5 ) - hash ) + char; 21 | /* eslint-disable-next-line no-bitwise */ 22 | hash = hash & hash; 23 | } 24 | 25 | if (hash < 0) { 26 | return hash * -1; 27 | } 28 | 29 | return hash; 30 | } 31 | 32 | public static randomStringOfLength(seed: number, requiredLength: number): string { 33 | const allowedLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 34 | 35 | let result = ""; 36 | for (let currentLength = 0; currentLength < requiredLength; ++currentLength) { 37 | seed = Utils.nextMinstdRandFor(seed); 38 | result += allowedLetters[Math.floor(allowedLetters.length * (seed / Utils.minstdRandMax))]; 39 | } 40 | 41 | return result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/graph.ts: -------------------------------------------------------------------------------- 1 | export default class Graph { 2 | private nodes: [string, T][]; 3 | private edges: [string, string][]; 4 | 5 | public constructor() { 6 | this.nodes = []; 7 | this.edges = []; 8 | } 9 | 10 | public addNode(key: string, value: T) { 11 | this.nodes.push([key, value]); 12 | } 13 | 14 | public addEdge(from: string, to: string) { 15 | this.edges.push([from, to]); 16 | } 17 | 18 | public removeNode(key: string) { 19 | this.nodes = this.nodes.filter((n) => n[0] !== key); 20 | this.edges = this.edges.filter((e) => e[1] !== key); 21 | } 22 | 23 | public toString(): string { 24 | const nodes = this.nodes.map(n => n[0]).join(", "); 25 | const edges = this.edges.map(e => e[0] + "->" + e[1]).join(", "); 26 | return nodes + "\n" + edges; 27 | } 28 | 29 | public countNodes(): number { 30 | return this.nodes.length; 31 | } 32 | 33 | public countEdges(): number { 34 | return this.edges.length; 35 | } 36 | 37 | public popLeaf(): T { 38 | for (const node of this.nodes) { 39 | let leaf = true; 40 | for (const edge of this.edges) { 41 | if (edge[0] === node[0]) { 42 | leaf = false; 43 | break; 44 | } 45 | } 46 | 47 | if (leaf === true) { 48 | this.removeNode(node[0]); 49 | return node[1]; 50 | } 51 | } 52 | 53 | throw Error("No leaf found: " + this); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/locals.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import Merge from "../src/merge"; 3 | import File from "../src/file"; 4 | import FileList from "../src/file_list"; 5 | 6 | describe("locals", () => { 7 | it("class with locals FRIENDS, not possible", () => { 8 | 9 | const abap1 = `CLASS zcl_locals_test DEFINITION PUBLIC FINAL CREATE PUBLIC. 10 | PUBLIC SECTION. 11 | CLASS-METHODS public_method . 12 | PROTECTED SECTION. 13 | PRIVATE SECTION. 14 | CLASS-METHODS private_method . 15 | ENDCLASS. 16 | CLASS ZCL_LOCALS_TEST IMPLEMENTATION. 17 | METHOD private_method. 18 | ENDMETHOD. 19 | METHOD public_method. 20 | lcl_abap_to_json=>run( ). 21 | ENDMETHOD. 22 | ENDCLASS.`; 23 | 24 | const abap2 = `CLASS lcl_abap_to_json DEFINITION FINAL. 25 | PUBLIC SECTION. 26 | CLASS-METHODS run. 27 | ENDCLASS. 28 | 29 | CLASS zcl_locals_test DEFINITION LOCAL FRIENDS lcl_abap_to_json. 30 | 31 | CLASS lcl_abap_to_json IMPLEMENTATION. 32 | METHOD run. 33 | zcl_locals_test=>private_method( ). 34 | ENDMETHOD. 35 | ENDCLASS.`; 36 | 37 | const files = new FileList(); 38 | files.push(new File("zcl_locals_test.clas.abap", abap1)); 39 | files.push(new File("zcl_locals_test.clas.locals_imp.abap", abap2)); 40 | files.push(new File("zmain.abap", "REPORT zmain.\n\nINCLUDE zinclude.")); 41 | files.push(new File("zinclude.abap", "WRITE / 'Hello World!'.")); 42 | expect(() => Merge.merge(files, "zmain")).to.throw("Cannot merge LOCAL FRIENDS"); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/file.ts: -------------------------------------------------------------------------------- 1 | type FileContent = string | Buffer; 2 | export default class File { 3 | private filename: string; 4 | private contents: FileContent; 5 | private isUsed: boolean = false; 6 | 7 | // object names are unique across packages in ABAP, so 8 | // the folder name is not part of this class 9 | public constructor(filename: string, content: FileContent) { 10 | this.filename = filename; 11 | this.contents = content; 12 | } 13 | 14 | public isBinary(): boolean { 15 | return typeof this.contents !== "string"; 16 | } 17 | 18 | public getName(): string { 19 | return this.filename.split(".")[0]; 20 | } 21 | 22 | public getFilename(): string { 23 | return this.filename; 24 | } 25 | 26 | public getContents(): string { 27 | if (this.isBinary()) throw Error(`Binary file accessed as string [${this.filename}]`); 28 | return this.contents as string; 29 | } 30 | 31 | public getBlob(): Buffer { 32 | if (!this.isBinary()) throw Error(`Text file accessed as blob [${this.filename}]`); 33 | return this.contents as Buffer; 34 | } 35 | 36 | public wasUsed(): boolean { 37 | return this.isUsed; 38 | } 39 | 40 | public markUsed() { 41 | this.isUsed = true; 42 | } 43 | 44 | public isABAP(): boolean { 45 | return this.filename.match(/.abap$/) !== null; 46 | } 47 | 48 | public isPROG(): boolean { 49 | return this.filename.match(/prog.abap$/) !== null; 50 | } 51 | 52 | public isMain(): boolean { 53 | if (!this.isPROG() || this.isBinary()) { 54 | return false; 55 | } 56 | 57 | return !!this.getContents().match(/^(\*|\s*")\s*@@abapmerge\s+main\s+void/i); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/skip_test_classes.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import File from "../src/file"; 3 | import Merge from "../src/merge"; 4 | import FileList from "../src/file_list"; 5 | 6 | describe("classes 1, test", () => { 7 | it("something1", () => { 8 | const files = new FileList(); 9 | 10 | files.push(new File("zmain.abap", "REPORT zmain.\n\nINCLUDE zinclude.")); 11 | files.push(new File("zinclude.abap", "WRITE / 'Hello World!'.")); 12 | files.push(new File( 13 | "zcl_abapgit_test_serialize.clas.abap", 14 | `CLASS zcl_abapgit_test_serialize DEFINITION PUBLIC CREATE PUBLIC FOR TESTING . 15 | ENDCLASS. 16 | CLASS zcl_abapgit_test_serialize IMPLEMENTATION. 17 | ENDCLASS.`)); 18 | 19 | const result = Merge.merge(files, "zmain"); 20 | expect(result).to.not.contain("zcl_abapgit_test_serialize"); 21 | }); 22 | }); 23 | 24 | describe("classes 2, remove test classes from friends", () => { 25 | it("something2", () => { 26 | const files = new FileList(); 27 | 28 | files.push(new File("zmain.abap", "REPORT zmain.\n\ndata: foo type ref to zcl_serialize.")); 29 | files.push(new File("zcl_serialize.clas.abap", `class zcl_serialize definition public global friends zcl_abapgit_test_serialize . 30 | public section. 31 | endclass. 32 | class zcl_serialize implementation. 33 | endclass.`)); 34 | files.push(new File( 35 | "zcl_abapgit_test_serialize.clas.abap", 36 | `CLASS zcl_abapgit_test_serialize DEFINITION PUBLIC CREATE PUBLIC FOR TESTING . 37 | ENDCLASS. 38 | CLASS zcl_abapgit_test_serialize IMPLEMENTATION. 39 | ENDCLASS.`)); 40 | 41 | const result = Merge.merge(files, "zmain"); 42 | expect(result).to.not.match(/zcl_abapgit_test_serialize/i); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/file_list.ts: -------------------------------------------------------------------------------- 1 | import File from "./file"; 2 | 3 | export default class FileList implements Iterable { 4 | private files: File[]; 5 | 6 | public constructor(files?: File[]) { 7 | this.files = files 8 | ? [].concat(files) 9 | : []; 10 | } 11 | 12 | public push(f: File) { 13 | this.files.push(f); 14 | } 15 | 16 | public concat(f: FileList) { 17 | this.files.push(...f.files); 18 | } 19 | 20 | public length(): number { 21 | return this.files.length; 22 | } 23 | 24 | public get(i: number): File { 25 | return this.files[i]; 26 | } 27 | 28 | public fileByName(name: string): File { 29 | name = name.toLowerCase(); 30 | const file = this.files.find(f => f.getName().toLowerCase() === name.toLowerCase() && f.isABAP()); 31 | if (file) { 32 | file.markUsed(); 33 | return file; 34 | } 35 | 36 | throw Error(`file not found: ${name}`); 37 | } 38 | 39 | public otherByName(name: string): File { 40 | name = name.toLowerCase(); 41 | const file = this.files.find(f => f.getFilename().toLowerCase() === name.toLowerCase() && !f.isABAP()); 42 | if (file) { 43 | file.markUsed(); 44 | return file; 45 | } 46 | 47 | throw Error(`file not found: ${name}`); 48 | } 49 | 50 | public validateAllFilesUsed(allowUnused?: boolean): void { 51 | const unusedFiles = this.files 52 | .filter(i => !i.wasUsed() && (i.isABAP() && !i.isMain())) 53 | .map(i => i.getFilename().toLowerCase()) 54 | .join(", "); 55 | 56 | if (unusedFiles) { 57 | const text = `Not all files used: [${unusedFiles}]`; 58 | if (allowUnused === true) { 59 | console.log(text); 60 | } else { 61 | throw Error(text); 62 | } 63 | } 64 | } 65 | 66 | public [Symbol.iterator](): IterableIterator { 67 | return this.files[Symbol.iterator](); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abapmerge", 3 | "version": "0.16.6", 4 | "description": "Merge ABAP INCLUDEs into single file", 5 | "bin": { 6 | "abapmerge": "./abapmerge" 7 | }, 8 | "main": "build/merge.js", 9 | "scripts": { 10 | "build": "tsc --pretty", 11 | "buildw": "tsc -w --pretty", 12 | "testw": "ts-mocha test/**/*.ts --watch --watch-files **/*.ts", 13 | "test": "npm run build && mocha --recursive --reporter progress build/test && npm run lint", 14 | "test:only": "ts-mocha test/**/*.ts", 15 | "lint": "eslint --ignore-pattern \"build/*\" --ignore-pattern \"*.mjs\"", 16 | "publish:minor": "npm --no-git-tag-version version minor && rm -rf build && npm install && npm run test && npm publish --access public", 17 | "publish:patch": "npm --no-git-tag-version version patch && rm -rf build && npm install && npm run test && npm publish --access public", 18 | "sample": "node ./abapmerge sample/ztest.prog.abap", 19 | "start": "ts-node src/index.ts" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/larshp/abapmerge.git" 24 | }, 25 | "keywords": [ 26 | "ABAP" 27 | ], 28 | "author": "Lars Hvam Petersen", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/larshp/abapmerge/issues" 32 | }, 33 | "homepage": "https://github.com/larshp/abapmerge#readme", 34 | "devDependencies": { 35 | "@types/chai": "^4.3.20", 36 | "@types/mocha": "^10.0.9", 37 | "@types/node": "^24.3.0", 38 | "@typescript-eslint/eslint-plugin": "^8.22.0", 39 | "@typescript-eslint/parser": "^8.25.0", 40 | "@eslint/compat": "^1.2.0", 41 | "chai": "^4.5.0", 42 | "eslint": "^9.21.0", 43 | "mocha": "^10.7.3", 44 | "ts-mocha": "^10.0.0", 45 | "ts-node": "^10.9.2", 46 | "typescript": "^5.7.3" 47 | }, 48 | "dependencies": { 49 | "commander": "^10.0.0", 50 | "fast-xml-parser": "^5.0.8" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/interface_parser.ts: -------------------------------------------------------------------------------- 1 | import File from "./file"; 2 | import Class from "./class"; 3 | 4 | export default class InterfaceParser { 5 | 6 | public static parse(f: File): Class { 7 | 8 | const self = f.getFilename().split(".")[0]; 9 | const ifDefinition = f.getContents().match(/^[\s\S]*(INTERFACE\s+\S+)\s+PUBLIC([\s\S]+)$/i); 10 | if (!ifDefinition || !ifDefinition[1] || !ifDefinition[2]) { 11 | throw "error parsing interface: " + f.getFilename(); 12 | } 13 | 14 | const dependencies = new Set(); 15 | 16 | // this should capture just elements of an interface "zif=>..." 17 | // if interface itself is referred, then it is probably a REF TO 18 | // REF TOs will be solved by deferred definitions are not taken into account 19 | // (maybe it is wrong by the way, as it helps to identify cyclic dependencies) 20 | const typeDeps = f.getContents().matchAll(/TYPE\s+([^.,\n]+)?(ZIF_\w+)(=?)/ig); 21 | if (typeDeps) { 22 | for (const dep of typeDeps) { 23 | const furtherIfElementMarker = dep[3]; 24 | if (!furtherIfElementMarker) { 25 | const typeAtrrs = dep[1]; 26 | if (typeAtrrs.endsWith("VALUE '")) { 27 | continue; 28 | } 29 | if (!/REF\s+TO/i.test(typeAtrrs)) { 30 | throw new Error(`Unexpected interface ref: ${dep.toString()}`); 31 | } 32 | continue; 33 | } 34 | const ifName = dep[2].toLowerCase(); 35 | if (ifName !== self) { 36 | dependencies.add(ifName); 37 | } 38 | } 39 | } 40 | 41 | const interfaceDeps = f.getContents().matchAll(/INTERFACES(:)?\s+(ZIF_\w+)/ig); 42 | if (interfaceDeps) { 43 | for (const dep of interfaceDeps) { 44 | const name = dep[2].toLowerCase(); 45 | if (name !== self) { 46 | dependencies.add(name); 47 | } 48 | } 49 | } 50 | 51 | return new Class( 52 | self, 53 | ifDefinition[1] + ifDefinition[2], 54 | false, 55 | "", 56 | [...dependencies.values()] as string[] 57 | ); 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/web.ts: -------------------------------------------------------------------------------- 1 | import File from "./file"; 2 | import FileList from "./file_list"; 3 | import Merge from "./merge"; 4 | 5 | let files: FileList; 6 | 7 | function fname(s: string): string { 8 | return s.split(".")[0]; 9 | } 10 | 11 | export function onClick(e) { 12 | let merged = ""; 13 | 14 | try { 15 | merged = Merge.merge(files, e.srcElement.text); 16 | } catch (err) { 17 | alert(err); 18 | return; 19 | } 20 | 21 | document.getElementById("filelist").innerHTML = 22 | "
" +
23 |     merged +
24 |     "
"; 25 | } 26 | 27 | function redraw() { 28 | let html = "Select main file:
"; 29 | 30 | for (let i = 0; i < files.length(); i++) { 31 | html = html + 32 | "" + 33 | files.get(i).getName() + 34 | "
"; 35 | } 36 | 37 | document.getElementById("filelist").innerHTML = html; 38 | 39 | for (let i = 0; i < files.length(); i++) { 40 | document.getElementById("myLink" + i).onclick = onClick; 41 | } 42 | } 43 | 44 | function reset() { 45 | files = new FileList(); 46 | document.getElementById("filelist").innerHTML = ""; 47 | } 48 | 49 | function setupReader(file) { 50 | const name = file.name; 51 | const reader = new FileReader(); 52 | 53 | reader.onload = function() { 54 | files.push(new File(fname(name), reader.result as string)); 55 | redraw(); 56 | }; 57 | 58 | reader.readAsText(file); 59 | } 60 | 61 | function handleFileSelect(event) { 62 | event.stopPropagation(); 63 | event.preventDefault(); 64 | 65 | const input = event.dataTransfer.files; 66 | 67 | reset(); 68 | 69 | for (let i = 0; i < input.length; i++) { 70 | setupReader(input[i]); 71 | } 72 | } 73 | 74 | function handleDragOver(evt) { 75 | evt.stopPropagation(); 76 | evt.preventDefault(); 77 | evt.dataTransfer.dropEffect = "copy"; // explicitly show this is a copy. 78 | } 79 | 80 | function setupListeners() { 81 | const dropZone = document.getElementById("drop_zone"); 82 | dropZone.addEventListener("dragover", handleDragOver, false); 83 | dropZone.addEventListener("drop", handleFileSelect, false); 84 | } 85 | 86 | document.body.onload = setupListeners; 87 | -------------------------------------------------------------------------------- /test/includes.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import Merge from "../src/merge"; 3 | import File from "../src/file"; 4 | import FileList from "../src/file_list"; 5 | 6 | describe("Global class no include", () => { 7 | it("Program and global class without includes", () => { 8 | 9 | const files = new FileList(); 10 | 11 | files.push(new File("zfoo.abap", 12 | `REPORT zfoo. 13 | START-OF-SELECTION. 14 | NEW zcl_main( )->run( ).`)); 15 | 16 | files.push(new File("zcl_main.clas.abap", 17 | `CLASS zcl_main DEFINITION. 18 | PUBLIC SECTION. 19 | METHODS run. 20 | ENDCLASS. 21 | 22 | CLASS zcl_main IMPLEMENTATION. 23 | METHOD run. 24 | WRITE / 'Hello World!'. 25 | ENDMETHOD. 26 | ENDCLASS.`)); 27 | 28 | const result = Merge.merge(files, "zfoo"); 29 | expect(result).to.be.a("string"); 30 | expect(result).to.include("World"); 31 | 32 | }); 33 | }); 34 | 35 | describe("Local class include", () => { 36 | it("Program with local class that contains include", () => { 37 | 38 | const files = new FileList(); 39 | 40 | files.push(new File("zfoo.abap", ` 41 | REPORT zfoo. 42 | 43 | CLASS main DEFINITION. 44 | PUBLIC SECTION. 45 | METHODS run. 46 | ENDCLASS. 47 | 48 | CLASS main IMPLEMENTATION. 49 | METHOD run. 50 | INCLUDE zhello. 51 | ENDMETHOD. 52 | ENDCLASS. 53 | 54 | START-OF-SELECTION. 55 | NEW main( )->run( ). 56 | `)); 57 | 58 | files.push(new File("zhello.abap", "WRITE / 'Hello World!'.")); 59 | 60 | const result = Merge.merge(files, "zfoo"); 61 | expect(result).to.be.a("string"); 62 | expect(result).to.include("World"); 63 | 64 | }); 65 | }); 66 | 67 | describe("Global class include", () => { 68 | it("Program and global class that contains include", () => { 69 | 70 | const files = new FileList(); 71 | 72 | files.push(new File("zfoo.abap", 73 | `REPORT zfoo. 74 | 75 | START-OF-SELECTION. 76 | NEW zcl_main( )->run( ).`)); 77 | 78 | files.push(new File("zcl_main.clas.abap", 79 | `CLASS zcl_main DEFINITION. 80 | PUBLIC SECTION. 81 | METHODS run. 82 | ENDCLASS. 83 | 84 | CLASS zcl_main IMPLEMENTATION. 85 | METHOD run. 86 | INCLUDE zhello. 87 | ENDMETHOD. 88 | ENDCLASS.`)); 89 | 90 | files.push(new File("zhello.abap", "WRITE / 'Hello World!'.")); 91 | 92 | const result = Merge.merge(files, "zfoo"); 93 | expect(result).to.be.a("string"); 94 | expect(result).to.include("World"); 95 | 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/abapmerge.svg)](https://badge.fury.io/js/abapmerge) 2 | 3 | # abapmerge 4 | 5 | Merge ABAP INCLUDEs into single file. Function groups are skipped 6 | 7 | ## Building 8 | 9 | - `npm install` 10 | - `npm test` 11 | - `abapmerge -h` - to see the parameters 12 | 13 | ## Invocation example 14 | 15 | Take `src/zabapgit.prog.abap`, merge it, rename the program (`report` statement) to `zabapgit_standalone` and save to `zabapgit.abap` in the current directory. 16 | 17 | - `abapmerge -f src/zabapgit.prog.abap -c zabapgit_standalone -o zabapgit.abap` 18 | 19 | ## How it works 20 | 21 | Abapmerge takes a path to the main report and analyzes its code and all files stored in the same directory and all sub-directories. 22 | 23 | The resulting code consists of the code of all found ABAP classes and interfaces, regardless of their production use in any part of the resulting report, and contents of ABAP includes found in the main report or the included reports. 24 | 25 | Abapmerge expects that the whole directory structure should result into a single executable program and, hence, if it finds an ABAP report that is not directly or indirectly included in the main report, abapmerge terminates its processing without issuing the input. 26 | 27 | Abapmerge requires file naming schema compatible with the schema used by [abapGit](https://github.com/larshp/abapgit). 28 | 29 | Global classes `FOR TESTING` are skipped. 30 | 31 | ## Pragmas 32 | 33 | Abapmerge supports pragmas that can be written inside an abap comment. If written as " comment, then indentation before " is also used for output. 34 | 35 | `@@abapmerge command params` 36 | 37 | Currently supported pragmas: 38 | - **include** {filename} > {string wrapper} 39 | - {filename} - path to the file relative to script execution dir (argv[0]) 40 | - {string wrapper} is a pattern where `$$` is replaced by the include line 41 | - `$$` is escaped - ' replaced to '' (to fit in abap string), use `$$$` to skip escaping 42 | - **include-base64** {filename} > {string wrapper} 43 | - same is `include` just that the data is encoded to base64, supposedly the data is binary. 44 | - **include-cua** {filename.xml} > {variable} 45 | - reads XML, presumably a serialized PROG/FUGR, and extracts GUI status (CUA) node from it. Then use it to fill `variable`. The `variable` is supposed to be of `zcl_abapgit_objects_program=>ty_cua` type. Required data definitions are also generated e.g. `'DATA ls_adm LIKE variable-adm'`. 46 | - **main** void 47 | - must be included at the very first line of a ABAP program that should be treated as a standalone main report and abamerge should not die with an error if the program is never included. 48 | 49 | ### Examples 50 | 51 | ```abap 52 | ... 53 | " @@abapmerge include somefile.txt > APPEND '$$' TO styletab. 54 | " @@abapmerge include-cua abapgit.prog.xml > ls_cua 55 | ... 56 | ``` 57 | -------------------------------------------------------------------------------- /src/class_list.ts: -------------------------------------------------------------------------------- 1 | import File from "./file"; 2 | import FileList from "./file_list"; 3 | import Class from "./class"; 4 | import Graph from "./graph"; 5 | import InterfaceParser from "./interface_parser"; 6 | import { ClassParser } from "./class_parser"; 7 | 8 | export default class ClassList { 9 | private interfaces: Class[]; 10 | private classes: Class[]; 11 | private exceptions: Class[]; 12 | private testclasses: Class[]; 13 | 14 | public constructor(list: FileList) { 15 | this.interfaces = []; 16 | this.classes = []; 17 | this.exceptions = []; 18 | this.testclasses = []; 19 | 20 | this.parseFiles(list); 21 | } 22 | 23 | public getResult(): string { 24 | return [ 25 | this.getDeferred(), 26 | this.getExceptions(), 27 | this.getInterfaces(), 28 | this.getDefinitions(), 29 | this.getImplementations(), 30 | ].join(""); 31 | } 32 | 33 | public getDeferred(): string { 34 | const classes = this.classes.reduce((a, c) => "CLASS " + c.getName() + " DEFINITION DEFERRED.\n" + a, ""); 35 | const interfaces = this.interfaces.reduce((a, c) => "INTERFACE " + c.getName() + " DEFERRED.\n" + a, ""); 36 | return interfaces + classes; 37 | } 38 | 39 | public getImplementations(): string { 40 | return this.classes.reduce((a, c) => c.getImplementation() + "\n" + a, ""); 41 | } 42 | 43 | public getDefinitions(): string { 44 | const g = this.buildDependenciesGraph(this.classes); 45 | 46 | let result = ""; 47 | while (g.countNodes() > 0) { 48 | const leaf = g.popLeaf(); 49 | result = result + leaf.getDefinition() + "\n"; 50 | } 51 | 52 | return result; 53 | } 54 | 55 | public getExceptions(): string { 56 | const g = this.buildDependenciesGraph(this.exceptions); 57 | 58 | let result = ""; 59 | while (g.countNodes() > 0) { 60 | const leaf = g.popLeaf(); 61 | result = result + leaf.getDefinition() + "\n" + leaf.getImplementation() + "\n"; 62 | } 63 | 64 | return result; 65 | } 66 | 67 | public getInterfaces(): string { 68 | const g = this.buildDependenciesGraph(this.interfaces); 69 | 70 | let result = ""; 71 | while (g.countNodes() > 0) { 72 | const leaf = g.popLeaf(); 73 | result = result + leaf.getDefinition() + "\n"; 74 | } 75 | 76 | return result; 77 | } 78 | 79 | private buildDependenciesGraph(list: Class[]): Graph { 80 | const g = new Graph(); 81 | 82 | for (const c of list) { 83 | const className = c.getName(); 84 | g.addNode(className, c); 85 | for (const d of c.getDependencies()) g.addEdge(className, d); 86 | } 87 | 88 | return g; 89 | } 90 | 91 | private parseFiles(list: FileList) { 92 | for (let i = 0; i < list.length(); i++) { 93 | const f = list.get(i); 94 | if (f.getFilename().match(/\.clas\.abap$/)) { 95 | f.markUsed(); 96 | this.pushClass(f, list); 97 | } else if (f.getFilename().match(/\.clas\.testclasses\.abap$/)) { 98 | f.markUsed(); 99 | } else if (f.getFilename().match(/\.intf\.abap$/)) { 100 | f.markUsed(); 101 | this.pushInterface(f); 102 | } 103 | } 104 | if (this.testclasses.length) { 105 | // patch after the fact, as we don't know if a class is a test class until we parse it 106 | const remover = ClassParser.createTestFriendsRemover(this.testclasses); 107 | this.classes = this.classes.map(remover); 108 | this.interfaces = this.interfaces.map(remover); 109 | } 110 | } 111 | 112 | private pushClass(f: File, list: FileList): void { 113 | const cls = ClassParser.parse(f, list); 114 | if (cls.isForTesting() === true) { 115 | this.testclasses.push(cls); 116 | return; // skip global test classes 117 | } 118 | if (cls.getName().match(/^.?CX_/i)) { 119 | // the DEFINITION DEFERRED does not work very well for exception classes 120 | this.exceptions.push(cls); 121 | } else { 122 | this.classes.push(cls); 123 | } 124 | } 125 | 126 | private pushInterface(f: File): void { 127 | this.interfaces.push(InterfaceParser.parse(f)); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import js from "@eslint/js"; 6 | import {FlatCompat} from "@eslint/eslintrc"; 7 | import {fixupConfigRules} from "@eslint/compat"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all, 16 | }); 17 | 18 | const fixup = fixupConfigRules(compat.extends( 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | )); 22 | 23 | export default [...fixup, { 24 | 25 | languageOptions: { 26 | globals: { 27 | ...globals.browser, 28 | ...globals.node, 29 | }, 30 | 31 | parser: tsParser, 32 | ecmaVersion: 5, 33 | sourceType: "module", 34 | 35 | parserOptions: { 36 | project: "tsconfig.json", 37 | tsconfigRootDir: __dirname, 38 | }, 39 | }, 40 | 41 | rules: { 42 | "@typescript-eslint/consistent-type-assertions": "error", 43 | "@typescript-eslint/dot-notation": "error", 44 | 45 | "@typescript-eslint/explicit-member-accessibility": ["error", { 46 | accessibility: "explicit", 47 | }], 48 | 49 | "@typescript-eslint/no-empty-function": "error", 50 | "@typescript-eslint/no-explicit-any": "off", 51 | "@typescript-eslint/no-inferrable-types": "off", 52 | "@typescript-eslint/no-require-imports": "error", 53 | 54 | "@typescript-eslint/no-shadow": ["error", { 55 | hoist: "all", 56 | }], 57 | 58 | "@typescript-eslint/no-unused-expressions": "error", 59 | "@typescript-eslint/no-use-before-define": "off", 60 | "@typescript-eslint/no-var-requires": "error", 61 | "@typescript-eslint/prefer-namespace-keyword": "error", 62 | 63 | "@typescript-eslint/triple-slash-reference": "error", 64 | "brace-style": ["error", "1tbs"], 65 | "comma-dangle": ["error", "always-multiline"], 66 | curly: ["error", "multi-line"], 67 | "default-case": "error", 68 | "eol-last": "error", 69 | eqeqeq: ["error", "smart"], 70 | "guard-for-in": "error", 71 | 72 | "id-blacklist": [ 73 | "error", 74 | "any", 75 | "Number", 76 | "number", 77 | "String", 78 | "string", 79 | "Boolean", 80 | "boolean", 81 | "Undefined", 82 | "undefined", 83 | ], 84 | 85 | "id-match": "error", 86 | 87 | "max-len": ["error", { 88 | code: 140, 89 | }], 90 | 91 | "no-bitwise": "error", 92 | "no-caller": "error", 93 | "no-cond-assign": "error", 94 | 95 | "no-console": ["error", { 96 | allow: [ 97 | "log", 98 | "warn", 99 | "dir", 100 | "timeLog", 101 | "assert", 102 | "clear", 103 | "count", 104 | "countReset", 105 | "group", 106 | "groupEnd", 107 | "table", 108 | "dirxml", 109 | "error", 110 | "groupCollapsed", 111 | "Console", 112 | "profile", 113 | "profileEnd", 114 | "timeStamp", 115 | "context", 116 | ], 117 | }], 118 | 119 | "no-debugger": "error", 120 | "no-empty": "error", 121 | "no-eval": "error", 122 | "no-fallthrough": "error", 123 | "no-invalid-this": "error", 124 | "no-multiple-empty-lines": "off", 125 | "no-new-wrappers": "error", 126 | "no-null/no-null": "off", 127 | "no-redeclare": "error", 128 | "no-trailing-spaces": "error", 129 | "no-unused-labels": "error", 130 | "no-var": "error", 131 | "one-var": ["error", "never"], 132 | radix: "error", 133 | 134 | "spaced-comment": ["error", "always", { 135 | markers: ["/"], 136 | }], 137 | 138 | "use-isnan": "error", 139 | }, 140 | }]; 141 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import File from "./file"; 4 | import FileList from "./file_list"; 5 | import Merge from "./merge"; 6 | import { Command } from "commander"; 7 | import PackageInfo from "../package.json"; 8 | 9 | interface ICliArgs { 10 | entryFilename: string; 11 | entryDir: string; 12 | skipFUGR: boolean; 13 | noFooter: boolean; 14 | allowUnused: boolean; 15 | newReportName: string; 16 | outputFile: string; 17 | } 18 | 19 | export class Logic { 20 | private static textFiles = new Set([".abap", ".xml", ".css", ".js"]); 21 | 22 | private static isTextFile(filepath: string): boolean { 23 | return Logic.textFiles.has(path.extname(filepath).toLowerCase()); 24 | } 25 | 26 | private static readFiles(dir: string, pre = ""): FileList { 27 | const files = fs.readdirSync(dir); 28 | const list = new FileList(); 29 | 30 | for (const file of files) { 31 | const filepath = path.join(dir, file); 32 | 33 | if (fs.lstatSync(filepath).isFile()) { 34 | if (Logic.isTextFile(filepath)) { 35 | const contents = fs 36 | .readFileSync(filepath, "utf8") 37 | .replace(/\t/g, " ") // remove tabs 38 | .replace(/\r/g, ""); // unify EOL 39 | list.push(new File(file, contents)); 40 | } else { 41 | const buffer = fs.readFileSync(filepath); 42 | list.push(new File(file, buffer)); 43 | } 44 | 45 | } else { 46 | list.concat(this.readFiles(filepath, path.join(pre, file))); 47 | } 48 | } 49 | 50 | return list; 51 | } 52 | 53 | public static parseArgs(args: string[]): ICliArgs { 54 | const commander = new Command(); 55 | commander 56 | .storeOptionsAsProperties(false) 57 | .description(PackageInfo.description) 58 | .version(PackageInfo.version) 59 | .option("-f, --skip-fugr", "ignore unused function groups", false) 60 | .option("-o, --output ", "output to a file (instead of stdout)") 61 | .option("--without-footer", "do not append footers", false) 62 | .option("--allow-unused", "allow unused files", false) 63 | .option( 64 | "-c, --change-report-name ", 65 | "changes report name in REPORT clause in source code", 66 | ) 67 | .arguments(""); 68 | 69 | commander.exitOverride((err) => { 70 | if (err.code === "commander.missingArgument") { 71 | throw Error("Specify entrypoint file name"); 72 | } 73 | process.exit(err.exitCode); 74 | }); 75 | commander.parse(args); 76 | 77 | if (!commander.args.length) { 78 | throw Error("Specify entrypoint file name"); 79 | } else if (commander.args.length > 1) { 80 | throw Error("Specify just one entrypoint"); 81 | } 82 | 83 | const entrypoint = commander.args[0]; 84 | const entryDir = path.dirname(entrypoint); 85 | const entryFilename = path.basename(entrypoint); 86 | const cmdOpts = commander.opts(); 87 | 88 | return { 89 | entryDir, 90 | entryFilename, 91 | skipFUGR: cmdOpts.skipFugr, 92 | noFooter: cmdOpts.withoutFooter, 93 | allowUnused: cmdOpts.allowUnused, 94 | newReportName: cmdOpts.changeReportName, 95 | outputFile: cmdOpts.output, 96 | }; 97 | } 98 | 99 | public static run(args: string[]) { 100 | try { 101 | let output = ""; 102 | const parsedArgs = Logic.parseArgs(args); 103 | const entrypoint = path.join(parsedArgs.entryDir, parsedArgs.entryFilename); 104 | if (!fs.existsSync(entrypoint)) { 105 | throw new Error(`File "${entrypoint}" does not exist`); 106 | } 107 | 108 | const entryObjectName = parsedArgs.entryFilename.split(".")[0]; 109 | output = Merge.merge( 110 | Logic.readFiles(parsedArgs.entryDir), 111 | entryObjectName, 112 | { 113 | skipFUGR: parsedArgs.skipFUGR, 114 | newReportName: parsedArgs.newReportName, 115 | allowUnused: parsedArgs.allowUnused, 116 | appendAbapmergeMarker: parsedArgs.noFooter === false, 117 | }, 118 | ); 119 | if (parsedArgs.outputFile) { 120 | fs.writeFileSync(parsedArgs.outputFile, output, "utf-8"); 121 | } else { 122 | process.stdout.write(output); 123 | } 124 | if (output === undefined) throw new Error("output undefined, hmm?"); 125 | return 0; 126 | } catch (e) { 127 | process.stderr.write(e.message || e.toString()); 128 | return 1; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/cli.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-rest-params */ 2 | import * as chai from "chai"; 3 | import { Logic } from "../src/cli"; 4 | import { join } from "path"; 5 | 6 | const expect = chai.expect; 7 | 8 | let stderr; 9 | let args: string[]; 10 | 11 | describe("CLI parse arguments", () => { 12 | 13 | beforeEach(() => { 14 | args = ["node_path", "abapmerge"]; 15 | }); 16 | 17 | it("no arguments", () => { 18 | expect(() => Logic.parseArgs(args)).to.throw("Specify entrypoint file name"); 19 | }); 20 | 21 | it("too many entry points", () => { 22 | args.push("entrypoint_1"); 23 | args.push("entrypoint_2"); 24 | expect(() => Logic.parseArgs(args)).to.throw("Specify just one entrypoint"); 25 | }); 26 | 27 | it.skip("entrypoint file does not exist", () => { 28 | // skip, file existence check moved to `run`, proper implemention would suppose memfs or similar 29 | args.push("entrypoint_1"); 30 | expect(() => Logic.parseArgs(args)).to.throw("File \"entrypoint_1\" does not exist"); 31 | }); 32 | 33 | it("entrypoint existing file", () => { 34 | args.push(join(__dirname, "entry.abap")); 35 | const parsedArgs = Logic.parseArgs(args); 36 | const parsedArgsExpected = { 37 | entryDir: __dirname, 38 | entryFilename: "entry.abap", 39 | skipFUGR: false, 40 | noFooter: false, 41 | newReportName: undefined, 42 | allowUnused: false, 43 | outputFile: undefined, 44 | }; 45 | chai.assert.isNotNull(parsedArgs); 46 | chai.assert.deepEqual(parsedArgs, parsedArgsExpected); 47 | }); 48 | 49 | it("skipFugr option", () => { 50 | args.push("-f"); 51 | args.push(join(__dirname, "entry.abap")); 52 | const parsedArgs = Logic.parseArgs(args); 53 | const parsedArgsExpected = { 54 | entryDir: __dirname, 55 | entryFilename: "entry.abap", 56 | skipFUGR: true, 57 | noFooter: false, 58 | newReportName: undefined, 59 | allowUnused: false, 60 | outputFile: undefined, 61 | }; 62 | chai.assert.isNotNull(parsedArgs); 63 | chai.assert.deepEqual(parsedArgs, parsedArgsExpected); 64 | }); 65 | 66 | it("noFooter option", () => { 67 | args.push("-f"); 68 | args.push("--without-footer"); 69 | args.push(join(__dirname, "entry.abap")); 70 | const parsedArgs = Logic.parseArgs(args); 71 | const parsedArgsExpected = { 72 | entryDir: __dirname, 73 | entryFilename: "entry.abap", 74 | skipFUGR: true, 75 | noFooter: true, 76 | newReportName: undefined, 77 | allowUnused: false, 78 | outputFile: undefined, 79 | }; 80 | chai.assert.isNotNull(parsedArgs); 81 | chai.assert.deepEqual(parsedArgs, parsedArgsExpected); 82 | }); 83 | 84 | it("newReportName option", () => { 85 | args.push("-f"); 86 | args.push("--without-footer"); 87 | args.push("-c"); 88 | args.push("znewname"); 89 | args.push(join(__dirname, "entry.abap")); 90 | const parsedArgs = Logic.parseArgs(args); 91 | const parsedArgsExpected = { 92 | entryDir: __dirname, 93 | entryFilename: "entry.abap", 94 | skipFUGR: true, 95 | noFooter: true, 96 | newReportName: "znewname", 97 | allowUnused: false, 98 | outputFile: undefined, 99 | }; 100 | chai.assert.isNotNull(parsedArgs); 101 | chai.assert.deepEqual(parsedArgs, parsedArgsExpected); 102 | }); 103 | 104 | it("outputFile option", () => { 105 | args.push("-o"); 106 | args.push("some.file"); 107 | args.push(join(__dirname, "entry.abap")); 108 | const parsedArgs = Logic.parseArgs(args); 109 | const parsedArgsExpected = { 110 | entryDir: __dirname, 111 | entryFilename: "entry.abap", 112 | skipFUGR: false, 113 | noFooter: false, 114 | newReportName: undefined, 115 | allowUnused: false, 116 | outputFile: "some.file", 117 | }; 118 | chai.assert.isNotNull(parsedArgs); 119 | chai.assert.deepEqual(parsedArgs, parsedArgsExpected); 120 | }); 121 | }); 122 | 123 | function captureStream(stream) { 124 | const oldWrite = stream.write; 125 | let buf = ""; 126 | stream.write = function(chunk) { 127 | buf += chunk.toString(); // chunk is a String or Buffer 128 | oldWrite.apply(stream, arguments); 129 | }; 130 | 131 | return { 132 | unhook: function unhook() { 133 | stream.write = oldWrite; 134 | }, 135 | captured: function() { 136 | return buf; 137 | }, 138 | }; 139 | } 140 | 141 | describe("Logic Run", () => { 142 | beforeEach(() => { 143 | args = ["node_path", "abapmerge"]; 144 | stderr = captureStream(process.stderr); 145 | }); 146 | 147 | it("entrypoint file name error", () => { 148 | Logic.run(args); 149 | expect(stderr.captured()).equal("error: missing required argument 'entrypoint'\nSpecify entrypoint file name"); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /src/merge.ts: -------------------------------------------------------------------------------- 1 | import FileList from "./file_list"; 2 | import PragmaProcessor from "./pragma"; 3 | import ClassList from "./class_list"; 4 | import AbapmergeMarker from "./abapmerge_marker"; 5 | import { CollectStatements } from "./collect_statements"; 6 | 7 | export default class Merge { 8 | private static files: FileList; 9 | private static classes: ClassList; 10 | 11 | public static merge(files: FileList, main: string, options?: { 12 | skipFUGR?: boolean; 13 | newReportName?: string; 14 | appendAbapmergeMarker?: boolean; 15 | allowUnused?: boolean; 16 | }): string { 17 | this.files = files; 18 | if (!options) options = {}; 19 | 20 | if (options.skipFUGR) { 21 | this.files = this.skipFUGR(this.files); 22 | } 23 | this.files = PragmaProcessor.process(this.files); 24 | this.classes = new ClassList(this.files); 25 | 26 | let result = this.analyze(main, this.files.fileByName(main).getContents(), options.newReportName); 27 | this.files.validateAllFilesUsed(options.allowUnused); 28 | 29 | if (options.appendAbapmergeMarker) { 30 | result += new AbapmergeMarker().render(); 31 | } 32 | 33 | return result; 34 | } 35 | 36 | private static skipFUGR(files: FileList): FileList { 37 | const filesWithoutFugrs = [...files].filter(f => !f.getFilename().match(/\.fugr\./)); 38 | return new FileList(filesWithoutFugrs); 39 | } 40 | 41 | private static analyze(main: string, contents: string, newReportName?: string) { 42 | let output = ""; 43 | let lines = CollectStatements.collect(contents).split("\n"); 44 | let isMainReport = false; 45 | 46 | let lineNo = 0; 47 | if (main !== null) { 48 | while (lineNo < lines.length) { 49 | let line = lines[lineNo++]; 50 | const regexReportClause = /(^\s*REPORT\s+)([\w/]+)(\s+[^.*]*\.|\s*\.)/im; 51 | const reportClauseMatches = line.match(regexReportClause); 52 | 53 | if (reportClauseMatches) { 54 | isMainReport = reportClauseMatches[2].toLowerCase() === main.toLowerCase().replace(/#/g, "/"); 55 | if (newReportName) { 56 | line = line.replace(regexReportClause, `$1${newReportName}$3`); 57 | } 58 | } 59 | 60 | output += line + "\n"; 61 | 62 | if (isMainReport) { 63 | isMainReport = false; 64 | break; 65 | } 66 | } 67 | 68 | while (lineNo < lines.length) { 69 | const line = lines[lineNo]; 70 | 71 | if (!line.match(/^((\*.*)|(\s*))$/im)) { 72 | const classLines = this.classes.getResult().split("\n"); 73 | classLines.pop(); 74 | // insert the class source code in the array at lineNo, this way they are analyzed later 75 | lines = [].concat(lines.slice(0, lineNo), classLines, lines.slice(lineNo, lines.length)); 76 | break; 77 | } 78 | 79 | output += line + "\n"; 80 | ++lineNo; 81 | } 82 | } 83 | 84 | for ( ; lineNo < lines.length; ++lineNo) { 85 | const line = lines[lineNo]; 86 | const includes = this.matchIncludeStatement(line); 87 | if (includes) { 88 | for (const include of includes) { 89 | output = output + 90 | this.comment(include) + 91 | this.analyze(null, this.files.fileByName(include).getContents()) + 92 | "\n"; 93 | } 94 | } else { 95 | output += line + "\n"; 96 | } 97 | } 98 | 99 | return output.replace(/\n{3}|\n{2}$/g, "\n"); 100 | } 101 | 102 | /** returns INCLUDE names if found in current line */ 103 | private static matchIncludeStatement(line: string): string[] | undefined { 104 | let include = line.match(/^\s*INCLUDE\s+(z\w+)\s*\./i); 105 | if (!include) { 106 | // try namespaced 107 | include = line.match(/^\s*INCLUDE\s+(\/\w+\/\w+)\s*\./i); 108 | if (include) { 109 | include[1] = include[1].replace(/\//g, "#"); 110 | } 111 | } 112 | if (!include) { 113 | // try chained 114 | const moo = line.matchAll(/^\s*INCLUDE\s*:\s+(z\w+)(?:,\s*(z\w+))*\s*\./gi); 115 | const found: string[] = []; 116 | for (const m of moo) { 117 | for (let i = 1; i < 10; i++) { 118 | const element = m[i]; 119 | if (element === undefined) { 120 | return found; 121 | } 122 | found.push(element); 123 | } 124 | } 125 | } 126 | if (!include) { 127 | // try chained namespaced 128 | const moo = line.matchAll(/^\s*INCLUDE\s*:\s+(\/\w+\/\w+)(?:,\s*(\/\w+\/\w+))*\s*\./gi); 129 | const found: string[] = []; 130 | for (const m of moo) { 131 | for (let i = 1; i < 10; i++) { 132 | const element = m[i]; 133 | if (element === undefined) { 134 | return found; 135 | } 136 | found.push(element); 137 | } 138 | } 139 | } 140 | if (include === null) { 141 | return undefined 142 | } 143 | return [include[1]]; 144 | } 145 | 146 | private static comment(name: string): string { 147 | return "****************************************************\n" + 148 | "* abapmerge - " + name.toUpperCase() + "\n" + 149 | "****************************************************\n"; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /test/pragmas.ts: -------------------------------------------------------------------------------- 1 | 2 | import { expect } from "chai"; 3 | import PragmaProcessor from "../src/pragma"; 4 | import File from "../src/file"; 5 | import FileList from "../src/file_list"; 6 | 7 | function buildFileList(mock) { 8 | const files = new FileList(); 9 | for (const [filename, data] of Object.entries(mock)) { 10 | if (Array.isArray(data)) { 11 | files.push(new File(filename, (data as string[]).join("\n"))); 12 | } else { 13 | files.push(new File(filename, data as Buffer)); 14 | } 15 | } 16 | return files; 17 | } 18 | 19 | describe("Pragma include", () => { 20 | it("include a file with pragma", () => { 21 | const files = buildFileList({ 22 | "zmain.abap": [ 23 | "REPORT zmain.", 24 | " \" @@abapmerge include some.txt > append '$$' to tab.", 25 | ], 26 | "some.txt": [ 27 | "Hello", 28 | "World", 29 | ], 30 | }); 31 | 32 | const newList = PragmaProcessor.process(files, { noComments: true }); 33 | expect(newList.length()).to.equal(2); 34 | 35 | const main = newList.get(0); 36 | 37 | expect(main.getContents()).to.equal([ 38 | "REPORT zmain.", 39 | " append 'Hello' to tab.", 40 | " append 'World' to tab.", 41 | ].join("\n")); 42 | 43 | const inc = newList.get(1); 44 | expect(inc.wasUsed()).to.equal(true); 45 | }); 46 | 47 | it("include a file with pragma, base64", () => { 48 | const files = buildFileList({ 49 | "zmain.abap": [ 50 | "REPORT zmain.", 51 | " \" @@abapmerge include-base64 some.blob > append '$$' to tab.", 52 | ], 53 | "some.blob": Buffer.from("\x40\x41"), 54 | }); 55 | 56 | const newList = PragmaProcessor.process(files, { noComments: true }); 57 | expect(newList.length()).to.equal(2); 58 | 59 | const main = newList.get(0); 60 | 61 | expect(main.getContents()).to.equal([ 62 | "REPORT zmain.", 63 | " append 'QEE=' to tab.", 64 | ].join("\n")); 65 | 66 | const inc = newList.get(1); 67 | expect(inc.wasUsed()).to.equal(true); 68 | }); 69 | 70 | it("include a cua from XML", () => { 71 | const files = buildFileList({ 72 | "zmain.abap": ` 73 | REPORT zmain. 74 | DATA ls_cua TYPE ty_cua. 75 | " @@abapmerge include-cua some.xml > ls_cua 76 | `.trim().split("\n"), 77 | "some.xml": ` 78 | 79 | 80 | 81 | 82 | 83 | SOME 84 | 85 | 86 | 87 | 000001 88 | 89 | 90 | 91 | DECIDE_DIALOG 92 | P 93 | 94 | 95 | 96 | 97 | CANCEL 98 | 001 99 | 100 | 101 | OK 102 | 002 103 | 104 | 105 | 106 | 107 | 108 | 109 | `, 110 | }); 111 | 112 | const newList = PragmaProcessor.process(files, { noComments: true }); 113 | expect(newList.length()).to.equal(2); 114 | 115 | const main = newList.get(0); 116 | 117 | // Keep the indentation ! (to match the files above) 118 | expect(main.getContents()).to.equal(` 119 | REPORT zmain. 120 | DATA ls_cua TYPE ty_cua. 121 | DATA ls_sta LIKE LINE OF ls_cua-sta. 122 | DATA ls_fun LIKE LINE OF ls_cua-fun. 123 | ls_cua-adm-pfkcode = '000001'. 124 | CLEAR ls_sta. 125 | ls_sta-code = 'DECIDE_DIALOG'. 126 | ls_sta-modal = 'P'. 127 | APPEND ls_sta TO ls_cua-sta. 128 | CLEAR ls_fun. 129 | ls_fun-code = 'CANCEL'. 130 | ls_fun-textno = '001'. 131 | APPEND ls_fun TO ls_cua-fun. 132 | CLEAR ls_fun. 133 | ls_fun-code = 'OK'. 134 | ls_fun-textno = '002'. 135 | APPEND ls_fun TO ls_cua-fun. 136 | `.trim()); 137 | 138 | const inc = newList.get(1); 139 | expect(inc.wasUsed()).to.equal(true); 140 | }); 141 | 142 | it("include a cua from XML, negative", () => { 143 | const files = buildFileList({ 144 | "zmain.abap": ` 145 | REPORT zmain. 146 | " @@abapmerge include-cua some.xml > ls_cua 147 | `.trim().split("\n"), 148 | "some.xml": ` 149 | 150 | 151 | 152 | 153 | 154 | 155 | 000001 156 | 157 | 158 | DECIDE_DIALOG 159 | P 160 | 161 | 162 | 163 | 164 | 165 | `, 166 | }); 167 | 168 | expect(() => PragmaProcessor.process(files, { noComments: true })).to.throw(); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /src/class_parser.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | import File from "./file"; 3 | import FileList from "./file_list"; 4 | import Class from "./class"; 5 | import Utils from "./utils"; 6 | 7 | export class AbapPublicClass { 8 | public name: string; 9 | public hash: string; 10 | public def: string; 11 | public imp: string; 12 | } 13 | 14 | export class ClassParser { 15 | 16 | public static anonymizeTypeName(prefix15: string, type: string, name: string): string { 17 | const typeSeed = Utils.hashCodeOf(type); 18 | const typeAlias = Utils.randomStringOfLength(typeSeed, 5); 19 | 20 | const nameSeed = Utils.hashCodeOf(name); 21 | const nameAlias = Utils.randomStringOfLength(nameSeed, 10); 22 | 23 | return typeAlias + prefix15 + nameAlias; 24 | } 25 | 26 | public static renameLocalType(oldName: string, newName: string, parent: string, oldCode: string): string { 27 | const occurrences = [ 28 | // interface declaration 29 | { context: "* renamed: " + parent + " :: " + oldName + "\n", 30 | regstr: "^(\s*interface\\s+)" + oldName + "(\\s*\\.)", 31 | }, 32 | // class declaration 33 | { context: "* renamed: " + parent + " :: " + oldName + "\n", 34 | regstr: "^(\s*class\\s+)" + oldName + "(\\s+definition\\b)", 35 | }, 36 | { context: "", 37 | regstr: "^([^*\"\\n]*\\b)" + oldName + "(\\b)", 38 | }, 39 | ]; 40 | 41 | let newCode = oldCode; 42 | for (const occurrence of occurrences) { 43 | const regexp = new RegExp(occurrence.regstr, "igm"); 44 | while (newCode.match(regexp)) { 45 | newCode = newCode.replace(regexp, occurrence.context + "$1" + newName + "$2"); 46 | } 47 | } 48 | 49 | return newCode; 50 | } 51 | 52 | public static buildLocalFileName(part: string, publicClass: AbapPublicClass): string { 53 | return publicClass.name + ".clas.locals_" + part + ".abap"; 54 | } 55 | 56 | public static findFileByName(filename: string, list: FileList ): File { 57 | filename = filename.toLowerCase(); 58 | 59 | const length = list.length(); 60 | for (let i = 0; i < length; ++i) { 61 | const file = list.get(i); 62 | if (filename === file.getFilename().toLowerCase()) { 63 | return file; 64 | } 65 | } 66 | 67 | return null; 68 | } 69 | 70 | public static parseLocalContents(part: string, publicClass: AbapPublicClass, local: string): void { 71 | let global = local + "\n" + publicClass[part]; 72 | 73 | const regex = /^\s*((CLASS)\s+(\w+)\s+DEFINITION\s*[^\.]*|(INTERFACE)\s+(\w+)\s*)\./gim; 74 | let definition; 75 | 76 | while ((definition = regex.exec(local)) !== null) { 77 | if (definition[1].toUpperCase().includes("DEFINITION LOCAL FRIENDS")) { 78 | throw "Cannot merge LOCAL FRIENDS"; 79 | } 80 | 81 | let type = definition[2]; 82 | let name = definition[3]; 83 | 84 | if (!type) { 85 | // not class, dealing with interface 86 | type = definition[4]; 87 | name = definition[5]; 88 | } 89 | 90 | const alias = ClassParser.anonymizeTypeName(publicClass.hash, type, name); 91 | global = ClassParser.renameLocalType(name, alias, publicClass.name, global); 92 | 93 | // prepend (definition)? deferred 94 | let decl = type + " " + alias; 95 | if (type.toLowerCase() === "class") { 96 | decl = decl + " DEFINITION"; 97 | } 98 | global = decl + " DEFERRED.\n" + global; 99 | 100 | // types of the local definitions are used in the (public|local) 101 | // implementation. so, after we must update the implementation after 102 | // we finish processing the local definitions. 103 | if (part === "def") { 104 | // this is the only difference between processing definitions and 105 | // implementations - which can be handled by one function otherwise 106 | publicClass.imp = ClassParser.renameLocalType(name, alias, publicClass.name, publicClass.imp); 107 | } 108 | } 109 | 110 | publicClass[part] = global; 111 | } 112 | 113 | public static tryProcessLocalFile(part: string, publicClass: AbapPublicClass, list: FileList): void { 114 | const filename = ClassParser.buildLocalFileName(part, publicClass); 115 | const file = ClassParser.findFileByName(filename, list); 116 | if (file === null) { 117 | return; 118 | } 119 | 120 | const contents = file.getContents(); 121 | ClassParser.parseLocalContents(part, publicClass, contents); 122 | file.markUsed(); 123 | } 124 | 125 | public static parse(f: File, list: FileList): Class { 126 | 127 | const match = f.getContents().match(/^(([\s\S])*ENDCLASS\.)\s*(CLASS(.|\s)*)$/i); 128 | if (!match || !match[1] || !match[2] || !match[3]) { 129 | throw "error parsing class: " + f.getFilename(); 130 | } 131 | 132 | const publicClass = new AbapPublicClass(); 133 | publicClass.name = f.getFilename().split(".")[0]; 134 | publicClass.hash = Utils.randomStringOfLength(Utils.hashCodeOf(publicClass.name), 15); 135 | publicClass.def = this.makeLocal(publicClass.name, match[1]); 136 | publicClass.imp = match[3]; 137 | 138 | // make sure we parse the imp part at the very first step! 139 | ClassParser.tryProcessLocalFile("imp", publicClass, list); 140 | // because the local definitions are used in the implementation. 141 | ClassParser.tryProcessLocalFile("def", publicClass, list); 142 | 143 | const superMatch = publicClass.def.match(/INHERITING FROM (Z\w+)/i); 144 | // console.dir(superMatch); 145 | const dependencies = []; 146 | if (superMatch && superMatch[1]) { 147 | dependencies.push(superMatch[1].toLowerCase()); 148 | } 149 | 150 | const testing = publicClass.def.match(/ FOR TESTING/i) !== null; 151 | 152 | return new Class(publicClass.name, publicClass.def, testing, publicClass.imp, dependencies); 153 | } 154 | 155 | private static makeLocal(name: string, s: string): string { 156 | const reg1 = new RegExp("CLASS\\s+" + name + "\\s+DEFINITION\\s+(ABSTRACT\\s+)?PUBLIC", "i"); 157 | 158 | const addAbstract = s.match(/\s+ABSTRACT\s+?PUBLIC/i) !== null; 159 | 160 | let ret = s.replace(reg1, "CLASS " + name + " DEFINITION" + (addAbstract ? " ABSTRACT" : "")); 161 | 162 | const reg2 = new RegExp("GLOBAL\\s+FRIENDS\\s+ZCL_", "i"); 163 | ret = ret.replace(reg2, "FRIENDS ZCL_"); 164 | return ret; 165 | } 166 | 167 | public static createTestFriendsRemover(testclasses: Class[]) { 168 | const isNotTest = (name: string) => !testclasses.find(tc => tc.getName().toUpperCase() === name.toUpperCase()); 169 | return (cls: Class) => { 170 | const reg = new RegExp(`CLASS\\s+${cls.getName()}\\s+DEFINITION[^\\.]*((?:global)?friends\\s+([^\\.]+))\\.`, "i"); 171 | const [frienddec, friendstr] = (cls.getDefinition().match(reg) || []).slice(1); 172 | if (!friendstr) return cls; 173 | const oldfriends = friendstr.split(/\s+/).filter(f => f); 174 | const newfriends = oldfriends.filter(isNotTest); 175 | if (newfriends.length === oldfriends.length) return cls; 176 | const newfd = newfriends.length ? frienddec.replace(friendstr, newfriends.join(" ")) : ""; 177 | const newdef = cls.getDefinition().replace(frienddec, newfd ); 178 | const impl = cls.getImplementation(); 179 | return new Class(cls.getName(), newdef, cls.isForTesting(), impl && impl.toString(), cls.getDependencies()); 180 | }; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /test/interface_parser.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import InterfaceParser from "../src/interface_parser"; 3 | import File from "../src/file"; 4 | 5 | describe("interface_parser 1", () => { 6 | it("parses interface", () => { 7 | const f = new File("zif_abapgit_gui_page.intf.abap", ` 8 | INTERFACE zif_abapgit_gui_page PUBLIC. 9 | INTERFACES zif_abapgit_gui_event_handler. 10 | INTERFACES zif_abapgit_gui_renderable. 11 | ALIASES on_event FOR zif_abapgit_gui_event_handler~on_event. 12 | ALIASES render FOR zif_abapgit_gui_renderable~render. 13 | ENDINTERFACE.`); 14 | 15 | const result = InterfaceParser.parse(f); 16 | 17 | expect(result.getDependencies().length).to.equal(2); 18 | expect(result.getDependencies()).to.contain("zif_abapgit_gui_event_handler"); 19 | expect(result.getDependencies()).to.contain("zif_abapgit_gui_renderable"); 20 | }); 21 | 22 | it("parses interface but skips self", () => { 23 | const f = new File("zif_my_interface.intf.abap", ` 24 | INTERFACE zif_my_interface PUBLIC. 25 | TYPES ty_x TYPE zif_common_types=>ty_type1. 26 | TYPES ty_y TYPE zif_my_interface=>ty_x. 27 | ENDINTERFACE.`); 28 | 29 | const result = InterfaceParser.parse(f); 30 | 31 | expect(result.getDependencies().length).to.equal(1); 32 | expect(result.getDependencies()).to.contain("zif_common_types"); 33 | }); 34 | 35 | it("parses interface with different indentations", () => { 36 | const f = new File("zif_my_interface.intf.abap", ` 37 | INTERFACE zif_my_interface PUBLIC. 38 | TYPES ty_x TYPE zif_common_types=>ty_type1. 39 | INTERFACES zif_some_other_if. 40 | ENDINTERFACE.`); 41 | 42 | const result = InterfaceParser.parse(f); 43 | 44 | expect(result.getDependencies().length).to.equal(2); 45 | expect(result.getDependencies()).to.contain("zif_common_types"); 46 | expect(result.getDependencies()).to.contain("zif_some_other_if"); 47 | }); 48 | 49 | it("parses interface with several refs to same if", () => { 50 | const f = new File("zif_my_interface.intf.abap", ` 51 | INTERFACE zif_my_interface PUBLIC. 52 | TYPES ty_x TYPE zif_common_types=>ty_type1. 53 | TYPES ty_y TYPE zif_common_types=>ty_type2. 54 | ENDINTERFACE.`); 55 | 56 | const result = InterfaceParser.parse(f); 57 | 58 | expect(result.getDependencies().length).to.equal(1); 59 | expect(result.getDependencies()).to.contain("zif_common_types"); 60 | }); 61 | 62 | it("parses interface with types to from another interface", () => { 63 | const f = new File("zif_my_interface.intf.abap", ` 64 | INTERFACE zif_my_interface PUBLIC. 65 | TYPES ty_x TYPE zif_common_types=>ty_type1. 66 | TYPES ty_z TYPE REF TO zif_some_other_if=>ty_obj. 67 | ENDINTERFACE.`); 68 | 69 | const result = InterfaceParser.parse(f); 70 | 71 | expect(result.getDependencies().length).to.equal(2); 72 | expect(result.getDependencies()).to.contain("zif_common_types"); 73 | expect(result.getDependencies()).to.contain("zif_some_other_if"); 74 | }); 75 | 76 | it("parses interface and skips REF TO another interface", () => { 77 | const f = new File("zif_my_interface.intf.abap", ` 78 | INTERFACE zif_my_interface PUBLIC. 79 | TYPES ty_z TYPE REF TO zif_some_other_if. 80 | TYPES ty_x TYPE TABLE OF REF TO zif_log. 81 | ENDINTERFACE.`); 82 | 83 | const result = InterfaceParser.parse(f); 84 | 85 | expect(result.getDependencies().length).to.equal(0); 86 | }); 87 | 88 | it("parses interface throws on missing REF TO and direct ref to interface", () => { 89 | const f = new File("zif_my_interface.intf.abap", ` 90 | INTERFACE zif_my_interface PUBLIC. 91 | TYPES ty_z TYPE zif_some_other_if~xyz. " ??? just to test it 92 | ENDINTERFACE.`); 93 | 94 | expect(() => InterfaceParser.parse(f)).throws(); 95 | }); 96 | 97 | it("parses interface with 'interfaces:'", () => { 98 | const f = new File("zif_my_interface.intf.abap", ` 99 | INTERFACE zif_my_interface PUBLIC. 100 | INTERFACES: zif_some_other_if. 101 | ENDINTERFACE.`); 102 | 103 | const result = InterfaceParser.parse(f); 104 | 105 | expect(result.getDependencies().length).to.equal(1); 106 | expect(result.getDependencies()).to.contain("zif_some_other_if"); 107 | }); 108 | 109 | it("parses interface with table types of types from another interface", () => { 110 | const f = new File("zif_my_interface.intf.abap", ` 111 | INTERFACE zif_my_interface PUBLIC. 112 | TYPES ty_x TYPE TABLE OF zif_common_types=>ty_type1. 113 | ENDINTERFACE.`); 114 | 115 | const result = InterfaceParser.parse(f); 116 | 117 | expect(result.getDependencies().length).to.equal(1); 118 | expect(result.getDependencies()).to.contain("zif_common_types"); 119 | }); 120 | 121 | it("parses interface and does not capture irrelevant references", () => { 122 | const f = new File("zif_my_interface.intf.abap", ` 123 | INTERFACE zif_my_interface PUBLIC. 124 | TYPES: 125 | ty_x TYPE i, " zif_common_types1 126 | ty_y TYPE i. " zif_common_types2 127 | ENDINTERFACE.`); 128 | 129 | const result = InterfaceParser.parse(f); 130 | 131 | expect(result.getDependencies().length).to.equal(0); 132 | }); 133 | 134 | it("parse apack", () => { 135 | const f = new File("zif_abapgit_apack_definitions.intf.abap", ` 136 | INTERFACE zif_abapgit_apack_definitions PUBLIC . 137 | 138 | TYPES: 139 | BEGIN OF ty_dependency, 140 | group_id TYPE string, 141 | artifact_id TYPE string, 142 | version TYPE string, 143 | sem_version TYPE zif_abapgit_definitions=>ty_version, 144 | git_url TYPE string, 145 | target_package TYPE devclass, 146 | END OF ty_dependency, 147 | ty_dependencies TYPE STANDARD TABLE OF ty_dependency 148 | WITH NON-UNIQUE DEFAULT KEY, 149 | 150 | ty_repository_type TYPE string, 151 | 152 | BEGIN OF ty_descriptor_wo_dependencies, 153 | group_id TYPE string, 154 | artifact_id TYPE string, 155 | version TYPE string, 156 | sem_version TYPE zif_abapgit_definitions=>ty_version, 157 | repository_type TYPE ty_repository_type, 158 | git_url TYPE string, 159 | END OF ty_descriptor_wo_dependencies, 160 | 161 | BEGIN OF ty_descriptor. 162 | INCLUDE TYPE ty_descriptor_wo_dependencies. 163 | TYPES: 164 | dependencies TYPE ty_dependencies, 165 | END OF ty_descriptor, 166 | 167 | ty_descriptors TYPE STANDARD TABLE OF ty_descriptor WITH NON-UNIQUE DEFAULT KEY. 168 | 169 | CONSTANTS c_dot_apack_manifest TYPE string VALUE '.apack-manifest.xml' ##NO_TEXT. 170 | CONSTANTS c_repository_type_abapgit TYPE ty_repository_type VALUE 'abapGit' ##NO_TEXT. 171 | CONSTANTS c_apack_interface_sap TYPE seoclsname VALUE 'IF_APACK_MANIFEST' ##NO_TEXT. 172 | CONSTANTS c_apack_interface_cust TYPE seoclsname VALUE 'ZIF_APACK_MANIFEST' ##NO_TEXT. 173 | ENDINTERFACE.`); 174 | 175 | const result = InterfaceParser.parse(f); 176 | 177 | expect(result.getDependencies().length).to.equal(1); 178 | }); 179 | 180 | it("parse zif_abapgit_repo_srv", () => { 181 | const f = new File("zif_abapgit_repo_srv.intf.abap", ` 182 | INTERFACE zif_abapgit_repo_srv 183 | PUBLIC . 184 | 185 | METHODS purge 186 | IMPORTING 187 | !ii_repo TYPE REF TO zif_abapgit_repo 188 | !is_checks TYPE zif_abapgit_definitions=>ty_delete_checks 189 | RETURNING 190 | VALUE(ri_log) TYPE REF TO zif_abapgit_log 191 | RAISING 192 | zcx_abapgit_exception . 193 | ENDINTERFACE.`); 194 | 195 | const result = InterfaceParser.parse(f); 196 | 197 | expect(result.getDependencies().length).to.equal(1); 198 | expect(result.getDependencies()[0]).to.equal("zif_abapgit_definitions"); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /src/pragma.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import FileList from "./file_list"; 3 | import File from "./file"; 4 | import { XMLParser } from "fast-xml-parser"; 5 | 6 | export interface IPragmaOpts { 7 | noComments?: boolean; 8 | } 9 | 10 | export default class PragmaProcessor { 11 | private files: FileList; 12 | private opts: IPragmaOpts; 13 | private currentPragmaName: string; 14 | 15 | public static process(files: FileList, opts?: IPragmaOpts): FileList { 16 | const instance = new PragmaProcessor(files, opts); 17 | return instance.processFiles(); 18 | } 19 | 20 | public constructor(files: FileList, opts?: IPragmaOpts) { 21 | this.files = files; 22 | this.opts = opts || {}; 23 | } 24 | 25 | public processFiles(): FileList { 26 | const newFiles = new FileList(); 27 | 28 | for (const file of this.files) { 29 | if (file.isBinary() || !file.isABAP()) { 30 | newFiles.push(file); 31 | continue; 32 | } 33 | 34 | const lines = file.getContents().split("\n"); 35 | let hasPragma = false; 36 | const output: string[] = []; 37 | 38 | for (const line of lines) { 39 | const pragma = line.match(/^(\*|(\s*)")\s*@@abapmerge\s+(.+)/i); 40 | if (pragma) { 41 | const pragmaCmd = pragma[3]; 42 | const indent = (pragma[1] === "*") ? "" : pragma[2]; 43 | const processed = this.processPragma(indent, pragmaCmd); 44 | if (processed && processed.length > 0) { 45 | hasPragma = true; 46 | output.push(...processed); 47 | } else { 48 | output.push(line); 49 | } 50 | } else { 51 | output.push(line); 52 | } 53 | } 54 | 55 | newFiles.push(hasPragma 56 | ? new File(file.getFilename(), output.join("\n")) 57 | : file); 58 | } 59 | 60 | return newFiles; 61 | } 62 | 63 | private comment(name: string): string[] { 64 | return this.opts.noComments 65 | ? [] 66 | : [ 67 | "****************************************************", 68 | `* abapmerge Pragma [${this.currentPragmaName}] - ${name.toUpperCase()}`, 69 | "****************************************************", 70 | ]; 71 | } 72 | 73 | private processPragma(indent: string, pragma: string): string[] | null { 74 | 75 | /* pragmas has the following format 76 | * @@abapmerge command params 77 | * if written as " comment, then indentation before " is also used for output 78 | * Currently supported pragmas: 79 | * - include {filename} > {string wrapper} 80 | * {filename} - path to the file relative to script execution dir (argv[0]) 81 | * {string wrapper} is a pattern where $$ is replaced by the include line 82 | * $$ is escaped - ' replaced to '' (to fit in abap string), use $$$ to avoid escaping 83 | * e.g. include somestyle.css > APPEND '$$' TO styletab. 84 | * - include-cua { filaname.xml } > { variable } 85 | * parse xml file, find CUA node and convert it to `variable` 86 | * zcl_abapgit_objects_program=>ty_cua type is expected for the var 87 | */ 88 | 89 | const result: string[] = []; 90 | const cmdMatch = pragma.match(/(\S+)\s+(.*)/); 91 | if (!cmdMatch) return null; 92 | const command = cmdMatch[1].toLowerCase(); 93 | const commandParams = cmdMatch[2]; 94 | 95 | this.currentPragmaName = command; // hack for `comment()` 96 | switch (command) { 97 | case "include": 98 | result.push(...this.pragmaInclude(indent, commandParams)); 99 | break; 100 | case "include-base64": 101 | result.push(...this.pragmaIncludeBase64(indent, commandParams)); 102 | break; 103 | case "include-cua": 104 | result.push(...this.pragmaIncludeCua(indent, commandParams)); 105 | break; 106 | 107 | default: break; 108 | } 109 | 110 | return result; 111 | } 112 | 113 | private pragmaInclude(indent: string, includeParams: string): string[] { 114 | const params = includeParams.match(/(\S+)\s*>\s*(.*)/i); 115 | if (!params) return []; 116 | const filename = params[1]; 117 | const template = params[2]; 118 | 119 | const lines = this.files.otherByName(filename).getContents().split("\n"); 120 | if (lines.length > 0 && !lines[lines.length - 1]) { 121 | lines.pop(); // remove empty string 122 | } 123 | 124 | const result: string[] = []; 125 | result.push(...this.comment(filename)); 126 | result.push(...lines.map(line => { 127 | let render = template.replace("$$$", line); // unescaped 128 | render = render.replace("$$", line.replace(/'/g, "''")); // escape ' -> '' 129 | return indent + render; 130 | })); 131 | 132 | return result; 133 | } 134 | 135 | private pragmaIncludeBase64(indent: string, includeParams: string): string[] { 136 | const params = includeParams.match(/(\S+)\s*>\s*(.*)/i); 137 | if (!params) return []; 138 | const filename = params[1]; 139 | const template = params[2]; 140 | 141 | const lines = this.files.otherByName(filename) 142 | .getBlob() 143 | .toString("base64") 144 | .match(/.{1,60}/g); 145 | if (!lines || lines.length === 0) return []; 146 | 147 | return [ 148 | ...this.comment(filename), 149 | ...lines.map(line => indent + template.replace(/\${2,3}/, line)), 150 | ]; 151 | } 152 | 153 | private pragmaIncludeCua(indent: string, includeParams: string): string[] { 154 | const params = includeParams.trim().match(/(\S+)\s*>\s*(\S+)/i); 155 | if (!params) return []; 156 | const filename = params[1]; 157 | const varName = params[2]; 158 | 159 | if (!/\.xml$/i.test(filename)) return []; 160 | 161 | const xml = this.files.otherByName(filename).getContents(); 162 | const lines = new CuaConverter().convert(xml, varName); 163 | if (!lines || lines.length === 0) return []; 164 | 165 | return [ 166 | ...this.comment(filename), 167 | ...lines.map(i => indent + i), 168 | ]; 169 | } 170 | 171 | } 172 | 173 | class CuaConverter { 174 | public convert(xml: string, varName: string): string[] | null { 175 | 176 | const parser = new XMLParser({ 177 | ignoreAttributes: true, 178 | numberParseOptions: { leadingZeros: false, hex: true }, // to keep leading zeros 179 | isArray: (_name, jpath: string, _isLeafNode, _isAttribute) => { 180 | // single item tables are still arrays (all but ADM branch) 181 | if (!jpath.startsWith("abapGit.asx:abap.asx:values.CUA.")) return false; 182 | const path = jpath.split("."); 183 | if (path.length !== 6) return false; 184 | if (path[4] === "ADM") return false; // flat structure 185 | return true; 186 | }, 187 | }); 188 | const parsed = parser.parse(xml); 189 | if (!parsed) throw Error("CuaConverter: XML parsing error"); 190 | const cua = parsed?.abapGit?.["asx:abap"]?.["asx:values"]?.CUA; 191 | if (!cua) return null; // cua is not found - nothing to convert but not an exception 192 | 193 | const defs: string[] = []; 194 | const code: string[] = []; 195 | 196 | // eslint-disable-next-line guard-for-in 197 | for (let key in cua) { 198 | let node = cua[key]; 199 | key = key.toLowerCase(); 200 | if (key === "adm") { 201 | code.push(...this.fillStruc(`${varName}-adm`, node)); 202 | } else { 203 | const itemName = `ls_${key}`; 204 | defs.push(`DATA ${itemName} LIKE LINE OF ${varName}-${key}.`); 205 | const attrs = Object.keys(node); 206 | if (attrs.length !== 1) throw Error(`CuaConverter: unexpected structure of [${key}] node`); 207 | node = node[attrs[0]]; 208 | if (!Array.isArray(node)) throw Error(`CuaConverter: unexpected structure of [${key}/${attrs[0]}] node`); 209 | for (const itemData of node) { 210 | code.push(`CLEAR ${itemName}.`); 211 | code.push(...this.fillStruc(itemName, itemData)); 212 | code.push(`APPEND ${itemName} TO ${varName}-${key}.`); 213 | } 214 | } 215 | } 216 | 217 | return [...defs, ...code]; 218 | } 219 | private fillStruc(varName: string, obj: object): string[] { 220 | return [...Object.entries(obj)].map(([key, val]) => `${varName}-${key.toLowerCase()} = '${val}'.`); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /test/class_list.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import File from "../src/file"; 3 | import FileList from "../src/file_list"; 4 | import ClassList from "../src/class_list"; 5 | 6 | const expect = chai.expect; 7 | 8 | describe("classes 1, test", () => { 9 | it("something", () => { 10 | const files = new FileList(); 11 | 12 | files.push(new File( 13 | "zcl_class.clas.abap", 14 | "CLASS zcl_class DEFINITION PUBLIC CREATE PUBLIC.\n" + 15 | " PUBLIC SECTION.\n" + 16 | " CLASS-METHODS: blah.\n" + 17 | "ENDCLASS.\n" + 18 | "CLASS zcl_class IMPLEMENTATION.\n" + 19 | " METHOD blah.\n" + 20 | " ENDMETHOD.\n" + 21 | "ENDCLASS.")); 22 | 23 | const classes = new ClassList(files); 24 | 25 | expect(classes.getDeferred().split("\n").length).to.equal(2); 26 | expect(classes.getDefinitions().split("\n").length).to.equal(5); 27 | expect(classes.getImplementations().split("\n").length).to.equal(5); 28 | expect(classes.getResult().split("\n").length).to.equal(10); 29 | }); 30 | }); 31 | 32 | describe("classes 2, parser error", () => { 33 | it("something", () => { 34 | 35 | const run = function () { 36 | const files = new FileList(); 37 | files.push(new File("zcl_class.clas.abap", "foo boo moo")); 38 | new ClassList(files); 39 | }; 40 | 41 | // eslint-disable-next-line no-invalid-this 42 | expect(run.bind(this)).to.throw("error parsing class: zcl_class.clas.abap"); 43 | }); 44 | }); 45 | 46 | describe("classes 3, remove public", () => { 47 | it("something", () => { 48 | const files = new FileList(); 49 | 50 | files.push(new File( 51 | "zcl_class.clas.abap", 52 | "CLASS zcl_class DEFINITION PUBLIC CREATE PUBLIC.\n" + 53 | " PUBLIC SECTION.\n" + 54 | " CLASS-METHODS: blah.\n" + 55 | "ENDCLASS.\n" + 56 | "CLASS zcl_class IMPLEMENTATION.\n" + 57 | " METHOD blah.\n" + 58 | " ENDMETHOD.\n" + 59 | "ENDCLASS.")); 60 | 61 | const classes = new ClassList(files); 62 | 63 | expect(classes.getDefinitions()).to.have.string("CLASS zcl_class DEFINITION CREATE PUBLIC."); 64 | }); 65 | }); 66 | 67 | describe("classes 4, exception class", () => { 68 | it("something", () => { 69 | const files = new FileList(); 70 | 71 | files.push(new File( 72 | "zcx_exception.clas.abap", 73 | "CLASS zcl_exception DEFINITION PUBLIC CREATE PUBLIC.\n" + 74 | " PUBLIC SECTION.\n" + 75 | " CLASS-METHODS: blah.\n" + 76 | "ENDCLASS.\n" + 77 | "CLASS zcx_exception IMPLEMENTATION.\n" + 78 | " METHOD blah.\n" + 79 | " ENDMETHOD.\n" + 80 | "ENDCLASS.")); 81 | 82 | const classes = new ClassList(files); 83 | 84 | expect(classes.getExceptions()).to.have.string("CLASS zcx_exception"); 85 | }); 86 | }); 87 | 88 | describe("classes 5, windows newline", () => { 89 | it("something", () => { 90 | const files = new FileList(); 91 | 92 | files.push(new File( 93 | "zcl_class.clas.abap", 94 | "CLASS zcl_class DEFINITION PUBLIC CREATE PUBLIC.\r\n" + 95 | " PUBLIC SECTION.\r\n" + 96 | " CLASS-METHODS: blah.\r\n" + 97 | "ENDCLASS.\r\n" + 98 | "CLASS zcl_class IMPLEMENTATION.\r\n" + 99 | " METHOD blah.\r\n" + 100 | " ENDMETHOD.\r\n" + 101 | "ENDCLASS.")); 102 | 103 | const classes = new ClassList(files); 104 | 105 | expect(classes.getDeferred().split("\n").length).to.equal(2); 106 | expect(classes.getDefinitions().split("\n").length).to.equal(5); 107 | expect(classes.getImplementations().split("\n").length).to.equal(5); 108 | expect(classes.getResult().split("\n").length).to.equal(10); 109 | }); 110 | }); 111 | 112 | 113 | describe("classes 6, interface", () => { 114 | it("something", () => { 115 | const files = new FileList(); 116 | 117 | files.push(new File( 118 | "zif_test.intf.abap", 119 | "INTERFACE zif_test PUBLIC.\n" + 120 | "TYPES: ty_type TYPE c LENGTH 6.\n" + 121 | "ENDINTERFACE.")); 122 | 123 | const classes = new ClassList(files); 124 | 125 | expect(classes.getInterfaces().split("\n").length).to.equal(4); 126 | }); 127 | }); 128 | 129 | describe("classes 7, sequenced by inheritance", () => { 130 | it("something", () => { 131 | const file1 = new File( 132 | "zcl_abapgit_syntax_abap.clas.abap", 133 | "CLASS zcl_abapgit_syntax_abap DEFINITION\n" + 134 | " PUBLIC\n" + 135 | " INHERITING FROM zcl_abapgit_syntax_highlighter\n" + 136 | " CREATE PUBLIC .\n" + 137 | "ENDCLASS.\n" + 138 | "CLASS zcl_abapgit_syntax_abap IMPLEMENTATION.\n" + 139 | "ENDCLASS."); 140 | 141 | const file2 = new File( 142 | "zcl_abapgit_syntax_highlighter.clas.abap", 143 | "CLASS zcl_abapgit_syntax_highlighter DEFINITION\n" + 144 | " PUBLIC\n" + 145 | " ABSTRACT\n" + 146 | " CREATE PUBLIC .\n" + 147 | "ENDCLASS.\n" + 148 | "CLASS zcl_abapgit_syntax_highlighter IMPLEMENTATION.\n" + 149 | "ENDCLASS."); 150 | 151 | const files = [file1, file2]; 152 | let classes = new ClassList(new FileList(files)); 153 | 154 | expect(classes.getDefinitions().split("\n")[0].indexOf("CLASS zcl_abapgit_syntax_highlighter")).to.equal(0); 155 | 156 | classes = new ClassList( new FileList(files.reverse())); 157 | expect(classes.getDefinitions().split("\n")[0].indexOf("CLASS zcl_abapgit_syntax_highlighter")).to.equal(0); 158 | }); 159 | }); 160 | 161 | describe("classes 8, exceptions sequenced by inheritance", () => { 162 | it("something", () => { 163 | const file1 = new File( 164 | "zcx_abapgit_2fa_unsupported.clas.abap", 165 | "CLASS zcx_abapgit_2fa_unsupported DEFINITION\n" + 166 | " PUBLIC\n" + 167 | " INHERITING FROM ZCX_ABAPGIT_2FA_ERROR\n" + 168 | " FINAL\n" + 169 | " CREATE PUBLIC .\n" + 170 | "ENDCLASS.\n" + 171 | "CLASS zcx_abapgit_2fa_unsupported IMPLEMENTATION.\n" + 172 | "ENDCLASS."); 173 | 174 | const file2 = new File( 175 | "zcx_abapgit_2fa_error.clas.abap", 176 | "CLASS zcx_abapgit_2fa_error DEFINITION\n" + 177 | " INHERITING FROM CX_STATIC_CHECK\n" + 178 | " CREATE PUBLIC .\n" + 179 | "ENDCLASS.\n" + 180 | "CLASS zcx_abapgit_2fa_error IMPLEMENTATION.\n" + 181 | "ENDCLASS."); 182 | 183 | const files = [file1, file2]; 184 | let classes = new ClassList(new FileList(files)); 185 | 186 | expect(classes.getExceptions().split("\n")[0].indexOf("CLASS zcx_abapgit_2fa_error")).to.equal(0); 187 | 188 | classes = new ClassList( new FileList(files.reverse())); 189 | expect(classes.getExceptions().split("\n")[0].indexOf("CLASS zcx_abapgit_2fa_error")).to.equal(0); 190 | }); 191 | }); 192 | 193 | describe("interfaces 1, dependencies", () => { 194 | it("something", () => { 195 | const file1 = new File( 196 | "zif_intf1.intf.abap", 197 | "INTERFACE zif_intf1 PUBLIC.\n" + 198 | "ENDINTERFACE."); 199 | 200 | const file2 = new File( 201 | "zif_intf2.intf.abap", 202 | "INTERFACE zif_intf2 PUBLIC.\n" + 203 | "METHODS read RETURNING VALUE(rt_foo) TYPE zif_intf1=>ty_moo.\n" + 204 | "METHODS blah RETURNING VALUE(rt_bar) TYPE zif_intf1=>ty_boo.\n" + 205 | "ENDINTERFACE."); 206 | 207 | const files = [file1, file2]; 208 | let classes = new ClassList(new FileList(files)); 209 | 210 | expect(classes.getInterfaces().split("\n")[0].indexOf("INTERFACE zif_intf1.")).to.equal(0); 211 | 212 | classes = new ClassList( new FileList(files.reverse())); 213 | expect(classes.getInterfaces().split("\n")[0].indexOf("INTERFACE zif_intf1.")).to.equal(0); 214 | }); 215 | }); 216 | 217 | describe("interfaces 2, reference to self", () => { 218 | it("something", () => { 219 | const file1 = new File( 220 | "zif_intf1.intf.abap", 221 | "INTERFACE zif_intf1 PUBLIC.\n" + 222 | "METHODS read RETURNING VALUE(rt_foo) TYPE zif_intf1=>ty_moo.\n" + 223 | "ENDINTERFACE."); 224 | 225 | const files = [file1]; 226 | const classes = new ClassList(new FileList(files)); 227 | 228 | expect(classes.getInterfaces().split("\n")[0].indexOf("INTERFACE zif_intf1.")).to.equal(0); 229 | }); 230 | }); 231 | 232 | describe("classes, remove PUBLIC, combined with abstract", () => { 233 | it("something", () => { 234 | const files = new FileList(); 235 | 236 | files.push(new File( 237 | "zcl_class.clas.abap", 238 | "CLASS zcl_class DEFINITION ABSTRACT PUBLIC CREATE PUBLIC.\n" + 239 | "ENDCLASS.\n" + 240 | "CLASS zcl_class IMPLEMENTATION.\n" + 241 | "ENDCLASS.")); 242 | 243 | const classes = new ClassList(files); 244 | 245 | expect(classes.getDefinitions()).to.have.string("CLASS zcl_class DEFINITION ABSTRACT CREATE PUBLIC."); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import Merge from "../src/merge"; 3 | import File from "../src/file"; 4 | import FileList from "../src/file_list"; 5 | 6 | describe("test 1, one include", () => { 7 | it("something", () => { 8 | const files = new FileList(); 9 | files.push(new File("zmain.abap", "REPORT zmain.\n\nINCLUDE zinclude.")); 10 | files.push(new File("zinclude.abap", "WRITE / 'Hello World!'.")); 11 | expect(Merge.merge(files, "zmain")).to.be.a("string"); 12 | }); 13 | }); 14 | 15 | describe("test 2, 2 includes", () => { 16 | it("something", () => { 17 | const files = new FileList(); 18 | files.push(new File("zmain.abap", "report zmain.\n\n" + 19 | "include zinc1.\n" + 20 | "include zinc2.\n\n" + 21 | "write / 'Main include'.")); 22 | files.push(new File("zinc1.abap", "write / 'hello @inc1'.")); 23 | files.push(new File("zinc2.abap", "write / 'hello @inc2'.")); 24 | expect(Merge.merge(files, "zmain")).to.be.a("string"); 25 | }); 26 | }); 27 | 28 | describe("test 3, subinclude", () => { 29 | it("something", () => { 30 | const files = new FileList(); 31 | files.push(new File("zmain.abap", "report zmain.\n\n" + 32 | "include zinc1.\n" + 33 | "include zinc2.\n\n" + 34 | "write / 'Main include'.")); 35 | files.push(new File("zinc1.abap", "include zsubinc1.\nwrite / 'hello @inc1'.")); 36 | files.push(new File("zinc2.abap", "write / 'hello @inc2'.")); 37 | files.push(new File("zsubinc1.abap", "write / 'hello @inc2'.")); 38 | expect(Merge.merge(files, "zmain")).to.be.a("string"); 39 | }); 40 | }); 41 | 42 | describe("test 4, standard include", () => { 43 | it("something", () => { 44 | const files = new FileList(); 45 | files.push(new File("zmain.abap", "report zmain.\n\n" + 46 | "include zinc1. \" A comment here\n" + 47 | "include zinc2.\n\n" + 48 | "write / 'Main include'.")); 49 | files.push(new File("zinc1.abap", "include standard.\nwrite / 'hello @inc1'.")); 50 | files.push(new File("zinc2.abap", "write / 'hello @inc2'.")); 51 | expect(Merge.merge(files, "zmain")).to.be.a("string"); 52 | }); 53 | }); 54 | 55 | describe("test 5, file not found", () => { 56 | it("something", () => { 57 | const files = new FileList(); 58 | files.push(new File("zmain.abap", "report zmain.\ninclude zinc1.")); 59 | expect(Merge.merge.bind(Merge, files, "zmain")).to.throw("file not found: zinc1"); 60 | }); 61 | }); 62 | 63 | describe("test 6, not all files used", () => { 64 | it("something", () => { 65 | const files = new FileList(); 66 | files.push(new File("zmain.abap", "report zmain.\ninclude zinc1.")); 67 | files.push(new File("zinc1.abap", "write / 'foo'.")); 68 | files.push(new File("zinc2.abap", "write / 'bar'.")); 69 | expect(Merge.merge.bind(Merge, files, "zmain")).to.throw("Not all files used: [zinc2.abap]"); 70 | }); 71 | }); 72 | 73 | describe("test 7, a unused README.md file", () => { 74 | it("something", () => { 75 | const files = new FileList(); 76 | files.push(new File("zmain.abap", "report zmain.\ninclude zinc1.")); 77 | files.push(new File("zinc1.abap", "write / 'foo'.")); 78 | files.push(new File("README.md", "foobar")); 79 | expect(Merge.merge(files, "zmain")).to.be.a("string"); 80 | }); 81 | }); 82 | 83 | describe("test 8, a unused README.md file", () => { 84 | it("something", () => { 85 | const files = new FileList(); 86 | files.push(new File("zmain.abap", "report zmain.\ninclude zinc1.")); 87 | files.push(new File("zinc1.abap", "write / 'foo'.")); 88 | files.push(new File("README.md", "foobar")); 89 | expect(Merge.merge(files, "zmain")).to.be.a("string"); 90 | }); 91 | }); 92 | 93 | describe("test 9, @@abapmerge commands", () => { 94 | it("something", () => { 95 | const files = new FileList(); 96 | files.push(new File("zmain.abap", "report zmain.\n" + 97 | "\n" + 98 | "write / 'Main include'.\n" + 99 | "* @@abapmerge include style.css > write '$$'.\n" + 100 | " \" @@abapmerge include js/script.js > write '$$'.\n" + 101 | " \" @@abapmerge wrong pragma, just copy to output\n" + 102 | " \" @@abapmerge include data.txt > write '$$'.\n" + 103 | " \" @@abapmerge include data.txt > write '$$$'. \" Unescaped !" )); 104 | files.push(new File("style.css", "body {\nbackground: red;\n}")); 105 | files.push(new File("data.txt", "content = 'X';\n")); 106 | files.push(new File("js/script.js", "alert(\"Hello world!\");\n")); 107 | const result = Merge.merge(files, "zmain"); 108 | expect(result).to.be.a("string"); 109 | expect(result.split("\n").length).to.equal(23); 110 | }); 111 | }); 112 | 113 | describe("test 10, one include, namespaced", () => { 114 | it("something", () => { 115 | const files = new FileList(); 116 | files.push(new File("zmain.abap", "REPORT zmain.\n\nINCLUDE /foo/zinclude.")); 117 | files.push(new File("#foo#zinclude.abap", "WRITE / 'Hello World!'.")); 118 | expect(Merge.merge(files, "zmain")).to.be.a("string"); 119 | }); 120 | }); 121 | 122 | describe("test 11, simple class", () => { 123 | it("something", () => { 124 | const files = new FileList(); 125 | files.push(new File("zmain.abap", "REPORT zmain.\n\nINCLUDE zinc1.")); 126 | files.push(new File("zinc1.abap", "write / 'foo'.")); 127 | files.push( 128 | new File("zcl_class.clas.abap", 129 | "CLASS zcl_class DEFINITION PUBLIC CREATE PUBLIC.\n" + 130 | " PUBLIC SECTION.\n" + 131 | " CLASS-METHODS: blah.\n" + 132 | "ENDCLASS.\n" + 133 | "CLASS zcl_class IMPLEMENTATION.\n" + 134 | " METHOD blah.\n" + 135 | " ENDMETHOD.\n" + 136 | "ENDCLASS.\n")); 137 | const result = Merge.merge(files, "zmain"); 138 | expect(result).to.be.a("string"); 139 | expect(result.split("\n").length).to.equal(17); 140 | }); 141 | }); 142 | 143 | describe("test 12, @@abapmerge in class", () => { 144 | it("something", () => { 145 | const files = new FileList(); 146 | files.push(new File("zmain.abap", "REPORT zmain.\n\nINCLUDE zinc1.")); 147 | files.push(new File("zinc1.abap", "write / 'foo'.")); 148 | files.push(new File("style.css", "body {\nbackground: red;\n}")); 149 | files.push( 150 | new File("zcl_class.clas.abap", 151 | "CLASS zcl_class DEFINITION PUBLIC CREATE PUBLIC.\n" + 152 | " PUBLIC SECTION.\n" + 153 | " CLASS-METHODS: blah.\n" + 154 | "ENDCLASS.\n" + 155 | "CLASS zcl_class IMPLEMENTATION.\n" + 156 | " METHOD blah.\n" + 157 | "* @@abapmerge include style.css > write '$$'.\n" + 158 | " ENDMETHOD.\n" + 159 | "ENDCLASS.")); 160 | 161 | const result = Merge.merge(files, "zmain"); 162 | expect(result).to.be.a("string"); 163 | expect(result.indexOf("background")).to.be.above(0); 164 | }); 165 | }); 166 | 167 | describe("test 13, skip function groups", () => { 168 | it("skip function groups with skipFUGR", () => { 169 | const files = new FileList([ 170 | new File("zmain.abap", "REPORT zmain."), 171 | new File("zabapgit_unit_te.fugr.saplzabapgit_unit_te.abap", "WRITE / 'Hello World!'."), 172 | ]); 173 | const merged = Merge.merge(files, "zmain", {skipFUGR: true}); 174 | expect(merged).to.be.a("string"); 175 | expect(merged).not.to.match(/Hello/); 176 | }); 177 | 178 | it("fails on function groups without skipFUGR", () => { 179 | const files = new FileList([ 180 | new File("zmain.abap", "REPORT zmain."), 181 | new File("zabapgit_unit_te.fugr.saplzabapgit_unit_te.abap", "WRITE / 'Hello World!'."), 182 | ]); 183 | expect(() => Merge.merge(files, "zmain", {skipFUGR: false})).to.throw(/Not all files used.*fugr/); 184 | }); 185 | }); 186 | 187 | describe("test 14, include classes event without INCLUDE", () => { 188 | it("include classes event without INCLUDE", () => { 189 | const files = new FileList(); 190 | files.push(new File("zmain13.prog.abap", "REPORT zmain13.\n" + 191 | "\n" + 192 | "write: 'Hello, world!'.\n")); 193 | files.push(new File("zcl_main.clas.abap", "class zcl_main defintion.\n" + 194 | "endclass.\n" + 195 | "class zcl_main implementation.\n" + 196 | "endclass.\n")); 197 | 198 | const exp = "REPORT zmain13.\n" + 199 | "\n" + 200 | "CLASS zcl_main DEFINITION DEFERRED.\n" + 201 | "class zcl_main defintion.\n" + 202 | "endclass.\n" + 203 | "class zcl_main implementation.\n" + 204 | "endclass.\n" + 205 | "\n" + 206 | "write: 'Hello, world!'.\n"; 207 | 208 | const result = Merge.merge(files, "zmain13"); 209 | 210 | expect(result).to.equal(exp); 211 | }); 212 | }); 213 | 214 | describe("test 15, included classes are placed after whitespace and comments", () => { 215 | it("something", () => { 216 | const files = new FileList(); 217 | files.push(new File("zmain13.prog.abap", "REPORT zmain13.\n" + 218 | "* Comment asterisk\n" + 219 | "write: 'Hello, world!'.\n")); 220 | 221 | files.push(new File("zcl_main.clas.abap", "class zcl_main defintion.\n" + 222 | "endclass.\n" + 223 | "class zcl_main implementation.\n" + 224 | "endclass.\n")); 225 | 226 | const exp = "REPORT zmain13.\n" + 227 | "* Comment asterisk\n" + 228 | "CLASS zcl_main DEFINITION DEFERRED.\n" + 229 | "class zcl_main defintion.\n" + 230 | "endclass.\n" + 231 | "class zcl_main implementation.\n" + 232 | "endclass.\n" + 233 | "\n" + 234 | "write: 'Hello, world!'.\n"; 235 | 236 | const result = Merge.merge(files, "zmain13"); 237 | 238 | expect(result).to.equal(exp); 239 | }); 240 | }); 241 | 242 | describe("test 16, REPORT with LINE-SIZE", () => { 243 | it("something", () => { 244 | const files = new FileList(); 245 | files.push(new File("zmain14.prog.abap", "REPORT zmain14 LINE-SIZE 100.\n" + 246 | "\n" + 247 | "write: 'Hello, world!'.\n")); 248 | files.push(new File("zcl_main.clas.abap", "class zcl_main defintion.\n" + 249 | "endclass.\n" + 250 | "class zcl_main implementation.\n" + 251 | "endclass.\n")); 252 | 253 | const exp = "REPORT zmain14 LINE-SIZE 100.\n" + 254 | "\n" + 255 | "CLASS zcl_main DEFINITION DEFERRED.\n" + 256 | "class zcl_main defintion.\n" + 257 | "endclass.\n" + 258 | "class zcl_main implementation.\n" + 259 | "endclass.\n" + 260 | "\n" + 261 | "write: 'Hello, world!'.\n"; 262 | 263 | const result = Merge.merge(files, "zmain14"); 264 | 265 | expect(result).to.equal(exp); 266 | }); 267 | }); 268 | 269 | describe("test 17, included classes are placed after whitespace and comments", () => { 270 | it("something", () => { 271 | const files = new FileList(); 272 | files.push(new File("zmain17.prog.abap", "REPORT zmain17 LINE-SIZE 100.\n" + 273 | "\n" + 274 | "* Comment asterisk\n" + 275 | "* Epic success!\n" + 276 | "\n" + 277 | "write: 'Hello, world!'.\n")); 278 | 279 | files.push(new File("zcl_main.clas.abap", "class zcl_main defintion.\n" + 280 | "endclass.\n" + 281 | "class zcl_main implementation.\n" + 282 | "endclass.\n")); 283 | 284 | const exp = "REPORT zmain17 LINE-SIZE 100.\n" + 285 | "\n" + 286 | "* Comment asterisk\n" + 287 | "* Epic success!\n" + 288 | "\n" + 289 | "CLASS zcl_main DEFINITION DEFERRED.\n" + 290 | "class zcl_main defintion.\n" + 291 | "endclass.\n" + 292 | "class zcl_main implementation.\n" + 293 | "endclass.\n" + 294 | "\n" + 295 | "write: 'Hello, world!'.\n"; 296 | 297 | const result = Merge.merge(files, "zmain17"); 298 | 299 | expect(result).to.equal(exp); 300 | }); 301 | }); 302 | 303 | describe("test 18, @@abapmerge w/ main no failure", () => { 304 | it("something", () => { 305 | const files = new FileList(); 306 | files.push(new File("zmain.abap", "REPORT zmain.\n\nINCLUDE zinc1.")); 307 | files.push(new File("zmain2.prog.abap", "\" @@abapmerge main void\n" + 308 | "REPORT zmain2.\n\nINCLUDE zinc1.")); 309 | files.push(new File("zinc1.abap", "write / 'foo'.")); 310 | 311 | const result = Merge.merge(files, "zmain"); 312 | expect(result).to.be.a("string"); 313 | }); 314 | }); 315 | 316 | describe("test 19, @@abapmerge w/o main causes failure", () => { 317 | it("something", () => { 318 | const files = new FileList(); 319 | files.push(new File("zmain.abap", "REPORT zmain.\n\nINCLUDE zinc1.")); 320 | files.push(new File("zmain2.prog.abap", "REPORT zmain2.\n\nINCLUDE zinc1.")); 321 | files.push(new File("zinc1.abap", "write / 'foo'.")); 322 | 323 | expect(Merge.merge.bind(Merge, files, "zmain")).to.throw("Not all files used: [zmain2.prog.abap]"); 324 | }); 325 | }); 326 | 327 | describe("test 20, abapmerge marker in footer", () => { 328 | it("abapmerge marker in footer", () => { 329 | const files = new FileList([ 330 | new File("zmain.abap", "REPORT zmain.\n\nINCLUDE zinc1."), 331 | new File("zinc1.abap", "write / 'foo'."), 332 | ]); 333 | 334 | const result = Merge.merge(files, "zmain", { 335 | appendAbapmergeMarker: true, 336 | }); 337 | expect(result).to.match(/\* abapmerge (?:(\d+\.[.\d]*\d+))/); 338 | expect(result).to.match(/^INTERFACE lif_abapmerge_marker\.$/m); 339 | }); 340 | }); 341 | 342 | describe("test 21, interface defintion should not cross 2 lines", () => { 343 | it("something", () => { 344 | const files = new FileList(); 345 | files.push(new File("zmain.abap", "REPORT zmain.\nINCLUDE zinc1.")); 346 | files.push(new File("zinc1.abap", "write / 'foo'.")); 347 | files.push(new File("zif_foo.intf.abap", "INTERFACE zif_foo\n PUBLIC.\nENDINTERFACE.")); 348 | const result = Merge.merge(files, "zmain"); 349 | const split = result.split("\n"); 350 | expect(result).to.be.a("string"); 351 | expect(split.indexOf("REPORT zmain.")).to.equal(0); 352 | expect(split.indexOf("INTERFACE zif_foo DEFERRED.")).to.equal(1); 353 | expect(split.indexOf("INTERFACE zif_foo.")).to.equal(2); 354 | }); 355 | }); 356 | 357 | describe("test 22, interface with comment", () => { 358 | it("something", () => { 359 | const files = new FileList(); 360 | files.push(new File("zmain.abap", "REPORT zmain.\nINCLUDE zinc1.")); 361 | files.push(new File("zinc1.abap", "write / 'foo'.")); 362 | files.push(new File("zif_foo.intf.abap", "* some comment\nINTERFACE zif_foo\n PUBLIC.\nENDINTERFACE.")); 363 | const result = Merge.merge(files, "zmain"); 364 | const split = result.split("\n"); 365 | expect(result).to.be.a("string"); 366 | expect(split.indexOf("REPORT zmain.")).to.equal(0); 367 | expect(split.indexOf("INTERFACE zif_foo DEFERRED.")).to.equal(1); 368 | expect(split.indexOf("INTERFACE zif_foo.")).to.equal(2); 369 | }); 370 | }); 371 | 372 | describe("test 23, replace report clause name by options", () => { 373 | it("something", () => { 374 | const files = new FileList(); 375 | files.push(new File("zmain.abap", "REPORT zmain.")); 376 | const result = Merge.merge(files, "zmain", {newReportName: "zmain_test"}); 377 | const split = result.split("\n"); 378 | expect(result).to.be.a("string"); 379 | expect(split.indexOf("REPORT zmain_test.")).to.equal(0); 380 | }); 381 | }); 382 | 383 | describe("test 24, replace report clause name by options with LINE-SIZE", () => { 384 | it("something", () => { 385 | const files = new FileList(); 386 | files.push(new File("zmain.abap", "REPORT zmain LINE-SIZE 100.")); 387 | const result = Merge.merge(files, "zmain", {newReportName: "zmain_test"}); 388 | const split = result.split("\n"); 389 | expect(result).to.be.a("string"); 390 | expect(split.indexOf("REPORT zmain_test LINE-SIZE 100.")).to.equal(0); 391 | }); 392 | }); 393 | 394 | describe("test 25, replace report clause name by options when report name has namespace", () => { 395 | it("something", () => { 396 | const files = new FileList(); 397 | files.push(new File("#prod#main.abap", "REPORT /prod/main.")); 398 | const result = Merge.merge(files, "#prod#main", {newReportName: "/prod/main_test"}); 399 | const split = result.split("\n"); 400 | expect(result).to.be.a("string"); 401 | expect(split.indexOf("REPORT /prod/main_test.")).to.equal(0); 402 | }); 403 | }); 404 | 405 | describe("test 26, namespaced includes", () => { 406 | it("something", () => { 407 | const files = new FileList(); 408 | files.push(new File("#foo#test.prog.abap", `REPORT /foo/test. 409 | 410 | INCLUDE /foo/inc. 411 | 412 | START-OF-SELECTION. 413 | PERFORM say_hello.`)); 414 | files.push(new File("#foo#inc.prog.abap", `FORM say_hello. 415 | WRITE / 'Hello World'. 416 | ENDFORM.`)); 417 | 418 | const result = Merge.merge(files, "#foo#test"); 419 | expect(result).to.be.a("string"); 420 | expect(result).to.include("FORM say_hello."); 421 | }); 422 | }); 423 | 424 | describe("test 27, chained includes", () => { 425 | it("something", () => { 426 | const files = new FileList(); 427 | files.push(new File("zfoo.prog.abap", 428 | `REPORT zfoo. 429 | INCLUDE: zfoo_inc, 430 | zfoo_inc2. 431 | START-OF-SELECTION. 432 | PERFORM do_nothing. 433 | PERFORM do_nothing2.`)); 434 | 435 | files.push(new File("zfoo_inc.prog.abap", `FORM do_nothing. 436 | RETURN. 437 | ENDFORM.`)); 438 | 439 | files.push(new File("zfoo_inc2.prog.abap", `FORM do_nothing2. 440 | RETURN. 441 | ENDFORM.`)); 442 | 443 | const result = Merge.merge(files, "zfoo"); 444 | expect(result).to.be.a("string"); 445 | expect(result).to.include("FORM do_nothing."); 446 | expect(result).to.include("FORM do_nothing2."); 447 | }); 448 | }); 449 | 450 | describe("test 28, REPORT comments", () => { 451 | it("something", () => { 452 | const files = new FileList(); 453 | files.push(new File("zfoo.prog.abap", 454 | `REPORT zfoo. "moo 455 | INCLUDE zfoo_inc. 456 | START-OF-SELECTION.PERFORM do_nothing.`)); 457 | files.push(new File("zfoo_inc.prog.abap", `FORM do_nothing. 458 | RETURN. 459 | ENDFORM.`)); 460 | 461 | const result = Merge.merge(files, "zfoo"); 462 | expect(result).to.be.a("string"); 463 | expect(result).to.include("FORM do_nothing."); 464 | }); 465 | }); 466 | 467 | describe("test 29, REPORT multi-line additions", () => { 468 | it("something", () => { 469 | const files = new FileList(); 470 | files.push(new File("zfoo.prog.abap", 471 | `REPORT zfoo 472 | LINE SIZE 100. 473 | INCLUDE zfoo_inc. 474 | START-OF-SELECTION. 475 | PERFORM do_nothing.`)); 476 | files.push(new File("zfoo_inc.prog.abap", `FORM do_nothing. 477 | RETURN. 478 | ENDFORM.`)); 479 | 480 | const result = Merge.merge(files, "zfoo"); 481 | expect(result).to.be.a("string"); 482 | expect(result).to.include("FORM do_nothing."); 483 | }); 484 | }); 485 | -------------------------------------------------------------------------------- /test/class_parser.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import File from "../src/file"; 3 | import FileList from "../src/file_list"; 4 | import {ClassParser, AbapPublicClass} from "../src/class_parser"; 5 | 6 | const expect = chai.expect; 7 | 8 | describe("class_parser 1, anonymizeTypeName contract", () => { 9 | it("something", () => { 10 | const prefix = "prefix"; 11 | const alias = ClassParser.anonymizeTypeName(prefix, "type", "name"); 12 | 13 | const anotherprefix = "foobar"; 14 | const anotheralias = ClassParser.anonymizeTypeName(anotherprefix, "type", "name"); 15 | 16 | expect(alias.length).to.equal(15 + prefix.length); 17 | expect(alias.substr(5, prefix.length)).to.equal(prefix); 18 | expect(alias.substr(0, 5)).to.equal(anotheralias.substr(0, 5)); 19 | expect(alias.substring(5 + prefix.length + 1, alias.length - 1)).to.equal( 20 | anotheralias.substring(5 + anotherprefix.length + 1, anotheralias.length - 1)); 21 | }); 22 | }); 23 | 24 | describe("class_parser 2, renameLocalType class member", () => { 25 | it("something", () => { 26 | const oldCode = "data(res) = cl_foo=>do_stuff()."; 27 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_bar", "cl_parent", oldCode); 28 | expect(newCode).to.equal("data(res) = cl_bar=>do_stuff()."); 29 | }); 30 | }); 31 | 32 | describe("class_parser 3, renameLocalType new", () => { 33 | it("something", () => { 34 | const oldCode = "data(res) = new cl_foo( )."; 35 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_bar", "cl_parent", oldCode); 36 | expect(newCode).to.equal("data(res) = new cl_bar( )."); 37 | }); 38 | }); 39 | 40 | describe("class_parser 4, renameLocalType ref to", () => { 41 | it("something", () => { 42 | const oldCode = "data: res type ref to cl_foo."; 43 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_bar", "cl_parent", oldCode); 44 | expect(newCode).to.equal("data: res type ref to cl_bar."); 45 | }); 46 | }); 47 | 48 | describe("class_parser 5, renameLocalType class definition", () => { 49 | it("something", () => { 50 | const oldCode = "class cl_foo definition."; 51 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_bar", "cl_parent", oldCode); 52 | expect(newCode).to.equal("* renamed: cl_parent :: cl_foo\n" + 53 | "class cl_bar definition."); 54 | }); 55 | }); 56 | 57 | describe("class_parser 6, renameLocalType class implementation", () => { 58 | it("something", () => { 59 | const oldCode = "class cl_foo implementation."; 60 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_bar", "cl_parent", oldCode); 61 | expect(newCode).to.equal("class cl_bar implementation."); 62 | }); 63 | }); 64 | 65 | describe("class_parser 7, renameLocalType interface declaration", () => { 66 | it("something", () => { 67 | const oldCode = "interface lif_foo.\nclass lcl_imp definition.\ninterfaces lif_foo.\nendclass.\n"; 68 | const newCode = ClassParser.renameLocalType("lif_foo", "lif_bar", "cl_parent", oldCode); 69 | 70 | expect(newCode).to.equal("* renamed: cl_parent :: lif_foo\n" + 71 | "interface lif_bar.\n" + 72 | "class lcl_imp definition.\ninterfaces lif_bar.\nendclass.\n"); 73 | }); 74 | }); 75 | 76 | describe("class_parser 8, buildLocalFileName imp", () => { 77 | it("something", () => { 78 | const abapClass = new AbapPublicClass(); 79 | abapClass.name = "cl_foo"; 80 | 81 | const filename = ClassParser.buildLocalFileName("imp", abapClass); 82 | 83 | expect(filename).to.equal("cl_foo.clas.locals_imp.abap"); 84 | }); 85 | }); 86 | 87 | describe("class_parser 9, buildLocalFileName def", () => { 88 | it("something", () => { 89 | const abapClass = new AbapPublicClass(); 90 | abapClass.name = "cl_foo"; 91 | 92 | const filename = ClassParser.buildLocalFileName("def", abapClass); 93 | 94 | expect(filename).to.equal("cl_foo.clas.locals_def.abap"); 95 | }); 96 | }); 97 | 98 | describe("class_parser 10, parseLocalContents contract class", () => { 99 | it("something", () => { 100 | const abapClass = new AbapPublicClass(); 101 | abapClass.name = "cl_foo"; 102 | abapClass.hash = "XOX"; 103 | abapClass.def = "lcl_def=>def_imp"; 104 | abapClass.imp = "lcl_def=>def_imp lcl_imp=>imp_only"; 105 | 106 | const oldLocalDef = "class lcl_def definition."; 107 | const oldLocalImp = "class lcl_imp definition."; 108 | 109 | ClassParser.parseLocalContents("imp", abapClass, oldLocalImp); 110 | 111 | expect(abapClass.def).to.equal("lcl_def=>def_imp"); 112 | expect(abapClass.imp).to.equal("class GiiGhXOXVndeoIopfZ DEFINITION DEFERRED.\n" + 113 | "* renamed: cl_foo :: lcl_imp\n" + 114 | "class GiiGhXOXVndeoIopfZ definition.\n" + 115 | "lcl_def=>def_imp GiiGhXOXVndeoIopfZ=>imp_only"); 116 | 117 | ClassParser.parseLocalContents("def", abapClass, oldLocalDef); 118 | 119 | expect(abapClass.def).to.equal("class GiiGhXOXPMwTAnXjaE DEFINITION DEFERRED.\n" + 120 | "* renamed: cl_foo :: lcl_def\n" + 121 | "class GiiGhXOXPMwTAnXjaE definition.\n" + 122 | "GiiGhXOXPMwTAnXjaE=>def_imp"); 123 | expect(abapClass.imp).to.equal("class GiiGhXOXVndeoIopfZ DEFINITION DEFERRED.\n" + 124 | "* renamed: cl_foo :: lcl_imp\n" + 125 | "class GiiGhXOXVndeoIopfZ definition.\n" + 126 | "GiiGhXOXPMwTAnXjaE=>def_imp GiiGhXOXVndeoIopfZ=>imp_only"); 127 | }); 128 | }); 129 | 130 | describe("class_parser 11, parseLocalContents contract interface", () => { 131 | it("something", () => { 132 | const abapClass = new AbapPublicClass(); 133 | abapClass.name = "cl_foo"; 134 | abapClass.hash = "XOX"; 135 | abapClass.def = "lif_def=>def_imp"; 136 | abapClass.imp = "lif_def=>def_imp lif_imp=>imp_only"; 137 | 138 | const oldLocalDef = "interface lif_def."; 139 | const oldLocalImp = "interface lif_imp."; 140 | 141 | ClassParser.parseLocalContents("imp", abapClass, oldLocalImp); 142 | 143 | expect(abapClass.def).to.equal("lif_def=>def_imp"); 144 | expect(abapClass.imp).to.equal("interface WboRqXOXAKOYzxghOo DEFERRED.\n" + 145 | "* renamed: cl_foo :: lif_imp\n" + 146 | "interface WboRqXOXAKOYzxghOo.\n" + 147 | "lif_def=>def_imp WboRqXOXAKOYzxghOo=>imp_only"); 148 | 149 | ClassParser.parseLocalContents("def", abapClass, oldLocalDef); 150 | 151 | expect(abapClass.def).to.equal("interface WboRqXOXujgMLcQaJU DEFERRED.\n" + 152 | "* renamed: cl_foo :: lif_def\n" + 153 | "interface WboRqXOXujgMLcQaJU.\n" + 154 | "WboRqXOXujgMLcQaJU=>def_imp"); 155 | expect(abapClass.imp).to.equal("interface WboRqXOXAKOYzxghOo DEFERRED.\n" + 156 | "* renamed: cl_foo :: lif_imp\n" + 157 | "interface WboRqXOXAKOYzxghOo.\n" + 158 | "WboRqXOXujgMLcQaJU=>def_imp WboRqXOXAKOYzxghOo=>imp_only"); 159 | }); 160 | }); 161 | 162 | describe("class_parser 12, parseLocalContents comments", () => { 163 | it("something", () => { 164 | const abapClass = new AbapPublicClass(); 165 | abapClass.name = "cl_foo"; 166 | abapClass.hash = "_XOX_"; 167 | abapClass.def = ""; 168 | abapClass.imp = ""; 169 | 170 | const oldLocalImp = "* interface foo definition.\n" + 171 | "class utils definition.\n"; 172 | 173 | ClassParser.parseLocalContents("imp", abapClass, oldLocalImp); 174 | 175 | expect(abapClass.def).to.equal(""); 176 | expect(abapClass.imp).to.equal("class GiiGh_XOX_QFKeljuxmY DEFINITION DEFERRED.\n" + 177 | "* interface foo definition.\n" + 178 | "* renamed: cl_foo :: utils\n" + 179 | "class GiiGh_XOX_QFKeljuxmY definition.\n\n"); 180 | }); 181 | }); 182 | 183 | describe("class_parser 13, findFileByName", () => { 184 | it("something", () => { 185 | const files = new FileList(); 186 | files.push(new File("camelCASEfile", "camelCASEfile")); 187 | files.push(new File("INVERTEDcaseFILE", "INVERTEDcaseFILE")); 188 | 189 | expect(ClassParser.findFileByName("CAMELcaseFILE", files).getContents()).to.equal("camelCASEfile"); 190 | expect(ClassParser.findFileByName("invertedCASEfile", files).getContents()).to.equal("INVERTEDcaseFILE"); 191 | }); 192 | }); 193 | 194 | describe("class_parser 14, tryProcessLocalFile not found imp", () => { 195 | it("something", () => { 196 | const abapClass = new AbapPublicClass(); 197 | abapClass.name = "cl_foo"; 198 | abapClass.def = ""; 199 | abapClass.imp = ""; 200 | 201 | const mainFile = new File("cl_foo.clas.abap", "class cl_foo definition."); 202 | const defFile = new File("cl_foo.clas.locals_def.abap", "interface lif_bar."); 203 | const files = new FileList(); 204 | files.push(mainFile); 205 | files.push(defFile); 206 | 207 | ClassParser.tryProcessLocalFile("imp", abapClass, files); 208 | 209 | expect(mainFile.wasUsed()).to.equal(false); 210 | expect(defFile.wasUsed()).to.equal(false); 211 | 212 | expect(abapClass.def).to.equal(""); 213 | expect(abapClass.imp).to.equal(""); 214 | }); 215 | }); 216 | 217 | describe("class_parser 15, tryProcessLocalFile not found def", () => { 218 | it("something", () => { 219 | const abapClass = new AbapPublicClass(); 220 | abapClass.name = "cl_foo"; 221 | abapClass.def = ""; 222 | abapClass.imp = ""; 223 | 224 | const mainFile = new File("cl_foo.clas.abap", "class cl_foo definition."); 225 | const impFile = new File("cl_foo.clas.locals_imp.abap", "class lcl_bar definition."); 226 | const files = new FileList(); 227 | files.push(mainFile); 228 | 229 | ClassParser.tryProcessLocalFile("def", abapClass, files); 230 | 231 | expect(mainFile.wasUsed()).to.equal(false); 232 | expect(impFile.wasUsed()).to.equal(false); 233 | 234 | expect(abapClass.def).to.equal(""); 235 | expect(abapClass.imp).to.equal(""); 236 | }); 237 | }); 238 | 239 | describe("class_parser 16, tryProcessLocalFile unknown", () => { 240 | it("something", () => { 241 | const abapClass = new AbapPublicClass(); 242 | abapClass.name = "cl_foo"; 243 | abapClass.def = ""; 244 | abapClass.imp = ""; 245 | 246 | const mainFile = new File("cl_foo.clas.abap", "class cl_foo definition."); 247 | const defFile = new File("cl_foo.clas.locals_def.abap", "interface lif_blah."); 248 | const impFile = new File("cl_foo.clas.locals_imp.abap", "class lcl_bar definition."); 249 | const files = new FileList(); 250 | files.push(mainFile); 251 | files.push(defFile); 252 | files.push(impFile); 253 | 254 | ClassParser.tryProcessLocalFile("omg", abapClass, files); 255 | 256 | expect(mainFile.wasUsed()).to.equal(false); 257 | expect(defFile.wasUsed()).to.equal(false); 258 | expect(impFile.wasUsed()).to.equal(false); 259 | 260 | expect(abapClass.def).to.equal(""); 261 | expect(abapClass.imp).to.equal(""); 262 | }); 263 | }); 264 | 265 | describe("class_parser 17, tryProcessLocalFile found", () => { 266 | it("something", () => { 267 | const abapClass = new AbapPublicClass(); 268 | abapClass.name = "cl_foo"; 269 | abapClass.def = ""; 270 | abapClass.imp = ""; 271 | 272 | const mainFile = new File("cl_foo.clas.abap", "class cl_foo definition."); 273 | const defFile = new File("cl_foo.clas.locals_def.abap", "* comment def"); 274 | const impFile = new File("cl_foo.clas.locals_imp.abap", "* comment imp"); 275 | const files = new FileList(); 276 | files.push(mainFile); 277 | files.push(defFile); 278 | files.push(impFile); 279 | 280 | ClassParser.tryProcessLocalFile("imp", abapClass, files); 281 | 282 | expect(mainFile.wasUsed()).to.equal(false); 283 | expect(defFile.wasUsed()).to.equal(false); 284 | expect(impFile.wasUsed()).to.equal(true); 285 | 286 | expect(abapClass.def).to.equal(""); 287 | expect(abapClass.imp).to.equal("* comment imp\n"); 288 | 289 | ClassParser.tryProcessLocalFile("def", abapClass, files); 290 | 291 | expect(mainFile.wasUsed()).to.equal(false); 292 | expect(defFile.wasUsed()).to.equal(true); 293 | expect(impFile.wasUsed()).to.equal(true); 294 | 295 | expect(abapClass.def).to.equal("* comment def\n"); 296 | expect(abapClass.imp).to.equal("* comment imp\n"); 297 | }); 298 | }); 299 | 300 | describe("class_parser 19, renameLocalType exception in catch with into", () => { 301 | it("something", () => { 302 | const oldCode = "catch cl_foo cl_bar into"; 303 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_new", "cl_parent", oldCode); 304 | expect(newCode).to.equal("catch cl_new cl_bar into"); 305 | 306 | const newCode2 = ClassParser.renameLocalType("cl_bar", "cl_future", "cl_parent", newCode); 307 | expect(newCode2).to.equal("catch cl_new cl_future into"); 308 | }); 309 | }); 310 | 311 | describe("class_parser 20, renameLocalType exception in catch w/o into", () => { 312 | it("something", () => { 313 | const oldCode = "catch cl_foo cl_bar."; 314 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_new", "cl_parent", oldCode); 315 | expect(newCode).to.equal("catch cl_new cl_bar."); 316 | 317 | const newCode2 = ClassParser.renameLocalType("cl_bar", "cl_future", "cl_parent", newCode); 318 | expect(newCode2).to.equal("catch cl_new cl_future."); 319 | }); 320 | }); 321 | 322 | describe("class_parser 21, renameLocalType exception in catch before unwind with into", () => { 323 | it("something", () => { 324 | const oldCode = "catch cl_foo cl_bar into"; 325 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_new", "cl_parent", oldCode); 326 | expect(newCode).to.equal("catch cl_new cl_bar into"); 327 | 328 | const newCode2 = ClassParser.renameLocalType("cl_bar", "cl_future", "cl_parent", newCode); 329 | expect(newCode2).to.equal("catch cl_new cl_future into"); 330 | }); 331 | }); 332 | 333 | describe("class_parser 22, renameLocalType exception in catch before unwind w/o into", () => { 334 | it("something", () => { 335 | const oldCode = "catch cl_foo cl_bar."; 336 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_new", "cl_parent", oldCode); 337 | expect(newCode).to.equal("catch cl_new cl_bar."); 338 | 339 | const newCode2 = ClassParser.renameLocalType("cl_bar", "cl_future", "cl_parent", newCode); 340 | expect(newCode2).to.equal("catch cl_new cl_future."); 341 | }); 342 | }); 343 | 344 | describe("class_parser 23, renameLocalType exception in raising", () => { 345 | it("something", () => { 346 | const oldCode = "raising cl_foo resumable(cl_bar)."; 347 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_new", "cl_parent", oldCode); 348 | expect(newCode).to.equal("raising cl_new resumable(cl_bar)."); 349 | 350 | const newCode2 = ClassParser.renameLocalType("cl_bar", "cl_future", "cl_parent", newCode); 351 | expect(newCode2).to.equal("raising cl_new resumable(cl_future)."); 352 | }); 353 | }); 354 | 355 | describe("class_parser 24, renameLocalType no rename in asterisk comments", () => { 356 | it("something", () => { 357 | const oldCode = "* The class cl_foo does the hard work"; 358 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_new", "cl_parent", oldCode); 359 | 360 | expect(newCode).to.equal("* The class cl_foo does the hard work"); 361 | }); 362 | }); 363 | 364 | describe("class_parser 25, renameLocalType no rename in double quote comments", () => { 365 | it("something", () => { 366 | const oldCode = "lo_inst = cl_foo=>instance( ). \" cache singletone inst of cl_foo"; 367 | const newCode = ClassParser.renameLocalType("cl_foo", "cl_new", "cl_parent", oldCode); 368 | 369 | expect(newCode).to.equal("lo_inst = cl_new=>instance( ). \" cache singletone inst of cl_foo"); 370 | }); 371 | }); 372 | 373 | describe("class_parser 26, renameLocalType interfaces in class definition", () => { 374 | it("something", () => { 375 | const oldCode = "class cl_parent definition.\n public section.\n interfaces lif_foo.\n"; 376 | const newCode = ClassParser.renameLocalType("lif_foo", "lif_renamed", "cl_parent", oldCode); 377 | 378 | expect(newCode).to.equal("class cl_parent definition.\n public section.\n interfaces lif_renamed.\n"); 379 | }); 380 | }); 381 | 382 | describe("class_parser 27, renameLocalType interface in method implementation", () => { 383 | it("something", () => { 384 | const oldCode = "class cl_parent implementation.\n method lif_foo~do.\n endmethod.\n"; 385 | const newCode = ClassParser.renameLocalType("lif_foo", "lif_renamed", "cl_parent", oldCode); 386 | 387 | expect(newCode).to.equal("class cl_parent implementation.\n method lif_renamed~do.\n endmethod.\n"); 388 | }); 389 | }); 390 | 391 | describe("class_parser 28, the method parse", () => { 392 | it("processes all files", () => { 393 | const mainFile = new File( 394 | "cl_parent.clas.abap", 395 | "class cl_parent definition.\n" + 396 | "private section.\n" + 397 | "data: mo_impl typ ref to lif_impl.\n" + 398 | "endclass.\n" + 399 | "class cl_parent implementation.\n" + 400 | "method constructor.\n" + 401 | "mo_impl = cast lif_impl(new lcl_impl_prod( )).\n" + 402 | "endmethod.\n" + 403 | "endclass.\n"); 404 | 405 | const defFile = new File( 406 | "cl_parent.clas.locals_def.abap", 407 | "interface lif_impl.\nendinterface.\n" + 408 | "class lcl_impl_base definition.\n" + 409 | "interfaces lif_impl.\n" + 410 | "endclass.\n"); 411 | 412 | const impFile = new File( 413 | "cl_parent.clas.locals_imp.abap", 414 | "class lcl_impl_prod definition inheriting from lcl_impl_base.\n" + 415 | "public section.\n" + 416 | "interfaces lif_impl.\n" + 417 | "endclass.\n"); 418 | 419 | const files = new FileList(); 420 | 421 | files.push(mainFile); 422 | files.push(defFile); 423 | files.push(impFile); 424 | 425 | const abapClass = ClassParser.parse(mainFile, files); 426 | 427 | expect(abapClass.getDefinition()).to.equal( 428 | "class GiiGhQvMEsAOOpdApbtQhfRrLQpNLF DEFINITION DEFERRED.\n" + 429 | "interface WboRqQvMEsAOOpdApbtQPWHIIjHuMM DEFERRED.\n" + 430 | "* renamed: cl_parent :: lif_impl\n" + 431 | "interface WboRqQvMEsAOOpdApbtQPWHIIjHuMM.\n" + 432 | "endinterface.\n" + 433 | "* renamed: cl_parent :: lcl_impl_base\n" + 434 | "class GiiGhQvMEsAOOpdApbtQhfRrLQpNLF definition.\n" + 435 | "interfaces WboRqQvMEsAOOpdApbtQPWHIIjHuMM.\n" + 436 | "endclass.\n\n" + 437 | "class cl_parent definition.\n" + 438 | "private section.\n" + 439 | "data: mo_impl typ ref to WboRqQvMEsAOOpdApbtQPWHIIjHuMM.\n" + 440 | "endclass."); 441 | 442 | expect(abapClass.getImplementation()).to.equal( 443 | "class GiiGhQvMEsAOOpdApbtQvMjIdUypid DEFINITION DEFERRED.\n* renamed: cl_parent :: lcl_impl_prod\n" + 444 | "class GiiGhQvMEsAOOpdApbtQvMjIdUypid definition inheriting from GiiGhQvMEsAOOpdApbtQhfRrLQpNLF.\n" + 445 | "public section.\ninterfaces WboRqQvMEsAOOpdApbtQPWHIIjHuMM.\nendclass.\n\n" + 446 | "class cl_parent implementation.\nmethod constructor.\n" + 447 | "mo_impl = cast WboRqQvMEsAOOpdApbtQPWHIIjHuMM(new GiiGhQvMEsAOOpdApbtQvMjIdUypid( )).\n" + 448 | "endmethod.\nendclass.\n"); 449 | }); 450 | }); 451 | 452 | describe("class_parser 29, multi occurrence on line", () => { 453 | it("processes all files", () => { 454 | const mainFile = new File( 455 | "cl_parent.clas.abap", 456 | ` 457 | class cl_parent definition. 458 | endclass. 459 | class cl_parent implementation. 460 | method constructor. 461 | CASE foo. 462 | WHEN lif_kind=>bar OR lif_kind=>bar. 463 | ENDCASE. 464 | endmethod. 465 | endclass.`); 466 | 467 | const defFile = new File( 468 | "cl_parent.clas.locals_def.abap", 469 | ` 470 | interface lif_kind. 471 | constants bar type c length 1 value 'A'. 472 | endinterface.`); 473 | 474 | const files = new FileList(); 475 | 476 | files.push(mainFile); 477 | files.push(defFile); 478 | 479 | const abapClass = ClassParser.parse(mainFile, files); 480 | 481 | expect(abapClass.getImplementation()).to.equal(`class cl_parent implementation. 482 | method constructor. 483 | CASE foo. 484 | WHEN WboRqQvMEsAOOpdApbtQdAMURRqLkF=>bar OR WboRqQvMEsAOOpdApbtQdAMURRqLkF=>bar. 485 | ENDCASE. 486 | endmethod. 487 | endclass.`); 488 | 489 | }); 490 | }); 491 | --------------------------------------------------------------------------------