@abaplint/transpiler 2.13.100 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/build/src/chunk.d.ts +3 -0
  2. package/build/src/chunk.js +6 -0
  3. package/build/src/db/cds_database_view.d.ts +19 -0
  4. package/build/src/db/cds_database_view.js +215 -0
  5. package/build/src/db/index.d.ts +1 -0
  6. package/build/src/db/index.js +18 -3
  7. package/build/src/expressions/compare.js +3 -3
  8. package/build/src/handlers/handle_abap.d.ts +3 -1
  9. package/build/src/handlers/handle_abap.js +7 -4
  10. package/build/src/handlers/handle_smim.d.ts +3 -0
  11. package/build/src/handlers/handle_smim.js +8 -1
  12. package/build/src/handlers/handle_w3mi.d.ts +3 -0
  13. package/build/src/handlers/handle_w3mi.js +8 -1
  14. package/build/src/index.d.ts +6 -5
  15. package/build/src/index.js +19 -23
  16. package/build/src/initialization.d.ts +5 -2
  17. package/build/src/initialization.js +19 -15
  18. package/build/src/output_layout.d.ts +17 -0
  19. package/build/src/output_layout.js +73 -0
  20. package/build/src/statements/assign.d.ts +3 -0
  21. package/build/src/statements/assign.js +19 -0
  22. package/build/src/statements/class_implementation.js +1 -1
  23. package/build/src/statements/method_implementation.js +10 -0
  24. package/build/src/statements/select.js +4 -4
  25. package/build/src/structures/interface.js +5 -2
  26. package/build/src/transpile_types.js +21 -2
  27. package/build/src/traversal.d.ts +2 -1
  28. package/build/src/traversal.js +48 -20
  29. package/build/src/types.d.ts +1 -12
  30. package/build/src/unit_test.d.ts +3 -0
  31. package/build/src/unit_test.js +8 -3
  32. package/build/src/validation.d.ts +2 -3
  33. package/build/src/validation.js +11 -10
  34. package/package.json +2 -2
@@ -39,6 +39,9 @@ export declare class Chunk {
39
39
  sourcePaths?: {
40
40
  [filename: string]: string;
41
41
  };
42
+ sourceContents?: {
43
+ [filename: string]: string;
44
+ };
42
45
  }): string;
43
46
  }
44
47
  export {};
@@ -218,6 +218,12 @@ class Chunk {
218
218
  original: m.original,
219
219
  generated: { line: m.generated.line + offset, column: m.generated.column },
220
220
  }));
221
+ for (const source of new Set(this.mappings.map(m => m.source))) {
222
+ const contents = options?.sourceContents?.[source];
223
+ if (contents !== undefined) {
224
+ sourceMapGenerator.setSourceContent(sourcePaths[source] ?? source, contents);
225
+ }
226
+ }
221
227
  const json = sourceMapGenerator.toJSON();
222
228
  json.file = generatedFilename;
223
229
  json.sourceRoot = "";
@@ -0,0 +1,19 @@
1
+ import * as abaplint from "@abaplint/core";
2
+ /** SQL for the supported relational subset of CDS. Never discard unknown syntax. */
3
+ export declare class CDSDatabaseView {
4
+ private readonly reg;
5
+ private viewName;
6
+ private readonly sources;
7
+ constructor(reg: abaplint.IRegistry);
8
+ build(obj: abaplint.Objects.DataDefinition): string | undefined;
9
+ private source;
10
+ private join;
11
+ private element;
12
+ private expression;
13
+ private field;
14
+ private fieldParts;
15
+ private name;
16
+ private keyword;
17
+ private quote;
18
+ private unsupported;
19
+ }
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CDSDatabaseView = void 0;
37
+ const abaplint = __importStar(require("@abaplint/core"));
38
+ const cds = abaplint.ExpressionsCDS;
39
+ /** SQL for the supported relational subset of CDS. Never discard unknown syntax. */
40
+ class CDSDatabaseView {
41
+ reg;
42
+ viewName = "";
43
+ sources = [];
44
+ constructor(reg) {
45
+ this.reg = reg;
46
+ }
47
+ build(obj) {
48
+ this.viewName = obj.getName();
49
+ const tree = new abaplint.CDSParser().parse(obj.findSourceFile());
50
+ if (tree === undefined) {
51
+ return this.unsupported("could not parse CDS definition");
52
+ }
53
+ else if (!(tree.get() instanceof cds.CDSDefineView)) {
54
+ return undefined; // Abstract/custom entities have no database view.
55
+ }
56
+ const name = tree.findDirectExpression(cds.CDSName);
57
+ const select = tree.findDirectExpression(cds.CDSSelect);
58
+ if (name === undefined || select === undefined) {
59
+ return this.unsupported("expected a SELECT view");
60
+ }
61
+ this.viewName = this.name(name);
62
+ for (const child of tree.getChildren()) {
63
+ if (child === name || child === select || child.get() instanceof cds.CDSAnnotation) {
64
+ continue;
65
+ }
66
+ this.keyword(child, ["DEFINE", "ROOT", "VIEW", "ENTITY", "AS", ";"]);
67
+ }
68
+ const source = select.findDirectExpression(cds.CDSSource);
69
+ if (source === undefined) {
70
+ return this.unsupported("missing source");
71
+ }
72
+ const from = [this.source(source)];
73
+ for (const join of select.findDirectExpressions(cds.CDSJoin)) {
74
+ from.push(this.join(join));
75
+ }
76
+ const elements = select.findDirectExpressions(cds.CDSElement);
77
+ if (elements.length === 0) {
78
+ return this.unsupported("expected explicit projected fields");
79
+ }
80
+ const columns = elements.map(e => this.element(e)).join(", ");
81
+ let where = "";
82
+ for (const child of select.getChildren()) {
83
+ if (child === source || child.get() instanceof cds.CDSJoin || child.get() instanceof cds.CDSElement) {
84
+ continue;
85
+ }
86
+ else if (child.get() instanceof cds.CDSWhere) {
87
+ where = " " + this.expression(child);
88
+ }
89
+ else {
90
+ this.keyword(child, ["SELECT", "DISTINCT", "FROM", "{", "}", ","]);
91
+ }
92
+ }
93
+ const distinct = select.findDirectTokenByText("DISTINCT") ? "DISTINCT " : "";
94
+ return `CREATE VIEW ${this.quote(this.viewName)} AS SELECT ${distinct}${columns} FROM ${from.join(" ")}${where};`;
95
+ }
96
+ source(node) {
97
+ const nameNode = node.findDirectExpression(cds.CDSPrefixedName);
98
+ if (nameNode === undefined) {
99
+ return this.unsupported("expected a table source");
100
+ }
101
+ const name = this.name(nameNode);
102
+ if (this.reg.getObject("TABL", name)?.getType() !== "TABL") {
103
+ return this.unsupported(`source ${name} does not resolve to a TABL`);
104
+ }
105
+ const as = node.findDirectExpression(cds.CDSAs);
106
+ const alias = as?.findDirectExpression(cds.CDSName) || node.findDirectExpression(cds.CDSName);
107
+ for (const child of node.getChildren()) {
108
+ if (child !== nameNode && child !== as && child !== alias) {
109
+ return this.unsupported("source parameters, filters or nested joins");
110
+ }
111
+ }
112
+ this.sources.push(alias ? this.name(alias) : name);
113
+ return this.quote(name) + (alias ? " AS " + this.quote(this.name(alias)) : "");
114
+ }
115
+ join(node) {
116
+ const sources = node.findDirectExpressions(cds.CDSSource);
117
+ const conditions = node.findDirectExpressions(cds.CDSCondition);
118
+ const words = node.getDirectTokens().map(t => t.getStr().toUpperCase()).join(" ");
119
+ const match = /^(?:(INNER|LEFT OUTER|RIGHT OUTER|CROSS) )?JOIN(?: ON)?$/.exec(words);
120
+ if (sources.length !== 1 || match === null) {
121
+ return this.unsupported("join shape: " + node.concatTokens());
122
+ }
123
+ const kind = match[1] || "INNER";
124
+ if (conditions.length !== (kind === "CROSS" ? 0 : 1)) {
125
+ return this.unsupported("join requires an ON condition (except CROSS JOIN)");
126
+ }
127
+ const source = this.source(sources[0]);
128
+ return kind + " JOIN " + source + (conditions.length ? " ON " + this.expression(conditions[0]) : "");
129
+ }
130
+ element(node) {
131
+ const field = node.findDirectExpression(cds.CDSPrefixedName);
132
+ const as = node.findDirectExpression(cds.CDSAs);
133
+ for (const child of node.getChildren()) {
134
+ if (child === field || child === as || child.get() instanceof cds.CDSAnnotation) {
135
+ continue;
136
+ }
137
+ this.keyword(child, ["KEY"]);
138
+ }
139
+ if (field === undefined) {
140
+ return this.unsupported("expected a projected source field");
141
+ }
142
+ const parts = this.fieldParts(field);
143
+ const alias = as?.findDirectExpression(cds.CDSName);
144
+ return this.field(field) + " AS " + this.quote(alias ? this.name(alias) : parts[parts.length - 1]);
145
+ }
146
+ expression(node) {
147
+ if (node instanceof abaplint.Nodes.TokenNode) {
148
+ return this.keyword(node, ["WHERE", "AND", "OR", "NOT", "IS", "NULL", "LIKE", "ESCAPE", "BETWEEN",
149
+ "=", "!", "<", ">", "(", ")"]);
150
+ }
151
+ else if (node.get() instanceof cds.CDSPrefixedName) {
152
+ if (node.getTokens().map(t => t.getStr()).join("").toLowerCase() === "$session.system_language") {
153
+ return "'E'";
154
+ }
155
+ return this.field(node);
156
+ }
157
+ else if (node.get() instanceof cds.CDSString || node.get() instanceof cds.CDSInteger) {
158
+ return node.getTokens().map(t => t.getStr()).join("");
159
+ }
160
+ else if (node.get() instanceof cds.CDSCondition || node.get() instanceof cds.CDSWhere) {
161
+ const parts = [];
162
+ for (const child of node.getChildren()) {
163
+ const part = this.expression(child);
164
+ if (["<", ">", "!"].includes(parts[parts.length - 1]) && ["=", ">"].includes(part)) {
165
+ parts[parts.length - 1] += part;
166
+ }
167
+ else {
168
+ parts.push(part);
169
+ }
170
+ }
171
+ return parts.join(" ");
172
+ }
173
+ return this.unsupported(node.concatTokens());
174
+ }
175
+ field(node) {
176
+ const parts = this.fieldParts(node);
177
+ if (parts.length === 1 && this.sources.length === 1) {
178
+ parts.unshift(this.sources[0]);
179
+ }
180
+ else if (parts.length === 2 && !this.sources.includes(parts[0])) {
181
+ return this.unsupported("unknown source qualifier " + parts[0]);
182
+ }
183
+ return parts.map(p => this.quote(p)).join(".");
184
+ }
185
+ fieldParts(node) {
186
+ const text = node.getTokens().map(t => t.getStr()).join("").toLowerCase();
187
+ const parts = text.split(".");
188
+ if (parts.length > 2 || parts.some(p => !/^(\/\w+\/)?\w+$/.test(p))) {
189
+ return this.unsupported("field path " + text);
190
+ }
191
+ return parts;
192
+ }
193
+ name(node) {
194
+ const name = node.getTokens().map(t => t.getStr()).join("").toLowerCase();
195
+ if (!/^(\/\w+\/)?\w+$/.test(name)) {
196
+ return this.unsupported("identifier " + name);
197
+ }
198
+ return name;
199
+ }
200
+ keyword(node, allowed) {
201
+ const text = node.concatTokens().toUpperCase();
202
+ if (!(node instanceof abaplint.Nodes.TokenNode) || !allowed.includes(text)) {
203
+ return this.unsupported(node.concatTokens());
204
+ }
205
+ return text;
206
+ }
207
+ quote(name) {
208
+ return '"' + name.toLowerCase().replace(/"/g, '""') + '"';
209
+ }
210
+ unsupported(detail) {
211
+ throw new Error(`CDS view ${this.viewName}: unsupported database definition: ${detail}`);
212
+ }
213
+ }
214
+ exports.CDSDatabaseView = CDSDatabaseView;
215
+ //# sourceMappingURL=cds_database_view.js.map
@@ -5,6 +5,7 @@ export declare class DatabaseSetup {
5
5
  private readonly reg;
6
6
  constructor(reg: abaplint.IRegistry);
7
7
  run(options?: ITranspilerOptions | undefined): DatabaseSetupResult;
8
+ private buildCDSViews;
8
9
  private driver;
9
10
  private buildInsert;
10
11
  }
@@ -39,6 +39,7 @@ const sqlite_database_schema_1 = require("./schema_generation/sqlite_database_sc
39
39
  const pg_database_schema_1 = require("./schema_generation/pg_database_schema");
40
40
  const snowflake_database_schema_1 = require("./schema_generation/snowflake_database_schema");
41
41
  const populate_tables_1 = require("./populate_tables");
42
+ const cds_database_view_1 = require("./cds_database_view");
42
43
  /////////////////////////
43
44
  // NOTES
44
45
  /////////////////////////
@@ -50,9 +51,11 @@ class DatabaseSetup {
50
51
  this.reg = reg;
51
52
  }
52
53
  run(options) {
54
+ // CDS views are dialect independent SQL, added after the tables and views they select from
55
+ const cdsViews = this.buildCDSViews();
53
56
  return {
54
57
  schemas: {
55
- sqlite: this.driver(new sqlite_database_schema_1.SQLiteDatabaseSchema(this.reg)),
58
+ sqlite: [...this.driver(new sqlite_database_schema_1.SQLiteDatabaseSchema(this.reg)), ...cdsViews],
56
59
  // HANA takes the PG schema unchanged -- measured against HANA Express
57
60
  // on a tree of 77 tables. The one thing it needs is that identifiers
58
61
  // reach it quoted in UPPER case, and that is not a dialect of DDL: the
@@ -61,13 +64,25 @@ class DatabaseSetup {
61
64
  // here. (Unquoted would nearly work and then meet a column called
62
65
  // `cross`, which is one of HANA's reserved words.)
63
66
  hdb: this.driver(new pg_database_schema_1.PGDatabaseSchema(this.reg)),
64
- pg: this.driver(new pg_database_schema_1.PGDatabaseSchema(this.reg)),
65
- snowflake: this.driver(new snowflake_database_schema_1.SnowflakeDatabaseSchema(this.reg)),
67
+ pg: [...this.driver(new pg_database_schema_1.PGDatabaseSchema(this.reg)), ...cdsViews],
68
+ snowflake: [...this.driver(new snowflake_database_schema_1.SnowflakeDatabaseSchema(this.reg)), ...cdsViews],
66
69
  },
67
70
  insert: this.buildInsert(options),
68
71
  };
69
72
  }
70
73
  ////////////////////
74
+ buildCDSViews() {
75
+ const views = [];
76
+ for (const obj of this.reg.getObjects()) {
77
+ if (obj instanceof abaplint.Objects.DataDefinition) {
78
+ const view = new cds_database_view_1.CDSDatabaseView(this.reg).build(obj);
79
+ if (view !== undefined) {
80
+ views.push(view);
81
+ }
82
+ }
83
+ }
84
+ return views;
85
+ }
71
86
  driver(schemaGenerator) {
72
87
  const statements = [];
73
88
  // CREATE TABLEs
@@ -53,12 +53,12 @@ class CompareTranspiler {
53
53
  const field = concat.replace(" IS NOT REQUESTED", "").toLowerCase();
54
54
  return new chunk_1.Chunk().appendString(pre + "INPUT && INPUT." + field + " === undefined && INPUT.importing?." + field + " === undefined");
55
55
  }
56
- if (concat.includes(" IS INSTANCE OF ")) {
57
- const notted = concat.startsWith("NOT ") || concat.includes(" IS NOT INSTANCE OF ");
56
+ if (concat.includes(" IS INSTANCE OF ") || concat.includes(" IS NOT INSTANCE OF ")) {
57
+ const notted = concat.startsWith("NOT ") !== concat.includes(" IS NOT INSTANCE OF ");
58
58
  const falsed = notted ? " === false" : "";
59
59
  const expr = node.findDirectExpression(core_1.Expressions.ClassName);
60
60
  const cname = expr?.concatTokens();
61
- const lookup = traversal.lookupClassOrInterface(cname, expr?.getFirstToken());
61
+ const lookup = cname?.toUpperCase() === "OBJECT" ? '"OBJECT"' : traversal.lookupClassOrInterface(cname, expr?.getFirstToken());
62
62
  return new chunk_1.Chunk().appendString("abap.compare.instance_of(").appendChunk(s0).appendString(`, ${lookup})` + falsed);
63
63
  }
64
64
  }
@@ -1,9 +1,11 @@
1
1
  import * as abaplint from "@abaplint/core";
2
2
  import { IOutputFile, ITranspilerOptions } from "../types";
3
3
  import { Chunk } from "../chunk";
4
+ import { OutputLayout } from "../output_layout";
4
5
  export declare class HandleABAP {
6
+ private readonly layout;
5
7
  private readonly options;
6
- constructor(options?: ITranspilerOptions);
8
+ constructor(options?: ITranspilerOptions, layout?: OutputLayout);
7
9
  runObject(obj: abaplint.ABAPObject, reg: abaplint.IRegistry): IOutputFile[];
8
10
  /** merges the locals def and imp into one mjs file */
9
11
  private rearrangeClassLocals;
@@ -39,10 +39,12 @@ const traversal_1 = require("../traversal");
39
39
  const requires_1 = require("../requires");
40
40
  const rearranger_1 = require("../rearranger");
41
41
  const chunk_1 = require("../chunk");
42
- const initialization_1 = require("../initialization");
42
+ const output_layout_1 = require("../output_layout");
43
43
  class HandleABAP {
44
+ layout;
44
45
  options;
45
- constructor(options) {
46
+ constructor(options, layout = new output_layout_1.OutputLayout()) {
47
+ this.layout = layout;
46
48
  this.options = options;
47
49
  }
48
50
  runObject(obj, reg) {
@@ -143,11 +145,12 @@ class HandleABAP {
143
145
  if (filename === output.filename) {
144
146
  continue;
145
147
  }
148
+ const specifier = (0, output_layout_1.importPath)(this.layout.file(output.object, output.filename), this.layout.sourceModule(r.filename));
146
149
  if (name) {
147
- contents.appendString("const {" + traversal_1.Traversal.escapeNamespace(name) + "} = await import(\"./" + (0, initialization_1.escapeFilenameForImport)(filename) + "\");\n");
150
+ contents.appendString("const {" + traversal_1.Traversal.escapeNamespace(name) + "} = await import(\"" + specifier + "\");\n");
148
151
  }
149
152
  else {
150
- contents.appendString("await import(\"./" + (0, initialization_1.escapeFilenameForImport)(filename) + "\");\n");
153
+ contents.appendString("await import(\"" + specifier + "\");\n");
151
154
  }
152
155
  }
153
156
  contents.appendChunk(output.chunk);
@@ -1,5 +1,8 @@
1
1
  import * as abaplint from "@abaplint/core";
2
2
  import { IOutputFile } from "../types";
3
+ import { OutputLayout } from "../output_layout";
3
4
  export declare class HandleSMIM {
5
+ private readonly layout;
6
+ constructor(layout?: OutputLayout);
4
7
  runObject(obj: abaplint.Objects.MIMEObject, _reg: abaplint.IRegistry): IOutputFile[];
5
8
  }
@@ -2,7 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HandleSMIM = void 0;
4
4
  const chunk_1 = require("../chunk");
5
+ const output_layout_1 = require("../output_layout");
5
6
  class HandleSMIM {
7
+ layout;
8
+ constructor(layout = new output_layout_1.OutputLayout()) {
9
+ this.layout = layout;
10
+ // Asset paths are relative to the output root.
11
+ }
6
12
  runObject(obj, _reg) {
7
13
  const filename = obj.getXMLFile()?.getFilename().replace(".xml", ".mjs").toLowerCase();
8
14
  if (filename === undefined) {
@@ -10,9 +16,10 @@ class HandleSMIM {
10
16
  }
11
17
  obj.parse();
12
18
  const dataFile = obj.getDataFile();
19
+ const dataFilename = dataFile ? this.layout.file({ name: obj.getName(), type: obj.getType() }, dataFile.getFilename()) : undefined;
13
20
  const chunk = new chunk_1.Chunk().appendString(`abap.SMIM["${obj.getName().toUpperCase()}"] = {
14
21
  "objectType": "SMIM",
15
- "filename": ${JSON.stringify(dataFile?.getFilename())},
22
+ "filename": ${JSON.stringify(dataFilename)},
16
23
  "url": ${JSON.stringify(obj.getURL())},
17
24
  "class": ${JSON.stringify(obj.getClass())},
18
25
  };`);
@@ -1,5 +1,8 @@
1
1
  import * as abaplint from "@abaplint/core";
2
2
  import { IOutputFile } from "../types";
3
+ import { OutputLayout } from "../output_layout";
3
4
  export declare class HandleW3MI {
5
+ private readonly layout;
6
+ constructor(layout?: OutputLayout);
4
7
  runObject(obj: abaplint.Objects.WebMIME, _reg: abaplint.IRegistry): IOutputFile[];
5
8
  }
@@ -2,8 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HandleW3MI = void 0;
4
4
  const chunk_1 = require("../chunk");
5
+ const output_layout_1 = require("../output_layout");
5
6
  const w3mi_name_1 = require("../w3mi_name");
6
7
  class HandleW3MI {
8
+ layout;
9
+ constructor(layout = new output_layout_1.OutputLayout()) {
10
+ this.layout = layout;
11
+ // Asset paths are relative to the output root.
12
+ }
7
13
  runObject(obj, _reg) {
8
14
  const filename = obj.getXMLFile()?.getFilename().replace(".xml", ".mjs").toLowerCase();
9
15
  if (filename === undefined) {
@@ -11,9 +17,10 @@ class HandleW3MI {
11
17
  }
12
18
  obj.parse();
13
19
  const dataFile = obj.getDataFile();
20
+ const dataFilename = dataFile ? this.layout.file({ name: obj.getName(), type: obj.getType() }, dataFile.getFilename()) : undefined;
14
21
  const chunk = new chunk_1.Chunk().appendString(`abap.W3MI["${(0, w3mi_name_1.w3miObjectName)(obj)}"] = {
15
22
  "objectType": "W3MI",
16
- "filename": ${JSON.stringify(dataFile?.getFilename())},
23
+ "filename": ${JSON.stringify(dataFilename)},
17
24
  };`);
18
25
  const output = {
19
26
  object: {
@@ -1,14 +1,15 @@
1
1
  import * as abaplint from "@abaplint/core";
2
2
  import { config } from "./validation";
3
- import { IFile, IOutput, IProgress, ITranspilerOptions, ITranspilerPlugin, IOutputFile, UnknownTypesEnum } from "./types";
3
+ import { IFile, IOutput, IProgress, ITranspilerOptions, IOutputFile, UnknownTypesEnum } from "./types";
4
4
  import { Chunk } from "./chunk";
5
5
  import { DatabaseSetupResult } from "./db/database_setup_result";
6
- export { config, ITranspilerOptions, ITranspilerPlugin, IFile, IProgress, IOutputFile, IOutput, UnknownTypesEnum, Chunk, DatabaseSetupResult };
6
+ import { OutputFolders } from "./output_layout";
7
+ export { OutputLayout, OutputFolders, importPath } from "./output_layout";
8
+ export { config, ITranspilerOptions, IFile, IProgress, IOutputFile, IOutput, UnknownTypesEnum, Chunk, DatabaseSetupResult };
7
9
  export declare class Transpiler {
8
10
  private readonly options;
9
- private readonly plugin;
10
- constructor(options?: ITranspilerOptions, plugin?: ITranspilerPlugin);
11
+ constructor(options?: ITranspilerOptions);
11
12
  runRaw(files: IFile[]): Promise<IOutput>;
12
- run(reg: abaplint.IRegistry, progress?: IProgress): Promise<IOutput>;
13
+ run(reg: abaplint.IRegistry, progress?: IProgress, folders?: OutputFolders): Promise<IOutput>;
13
14
  protected validate(reg: abaplint.IRegistry): void;
14
15
  }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Transpiler = exports.Chunk = exports.UnknownTypesEnum = exports.config = void 0;
36
+ exports.Transpiler = exports.Chunk = exports.UnknownTypesEnum = exports.config = exports.importPath = exports.OutputLayout = void 0;
37
37
  const abaplint = __importStar(require("@abaplint/core"));
38
38
  const validation_1 = require("./validation");
39
39
  Object.defineProperty(exports, "config", { enumerable: true, get: function () { return validation_1.config; } });
@@ -57,12 +57,14 @@ const handle_msag_1 = require("./handlers/handle_msag");
57
57
  const handle_oa2p_1 = require("./handlers/handle_oa2p");
58
58
  const handle_fugr_1 = require("./handlers/handle_fugr");
59
59
  const initialization_1 = require("./initialization");
60
+ const output_layout_1 = require("./output_layout");
61
+ var output_layout_2 = require("./output_layout");
62
+ Object.defineProperty(exports, "OutputLayout", { enumerable: true, get: function () { return output_layout_2.OutputLayout; } });
63
+ Object.defineProperty(exports, "importPath", { enumerable: true, get: function () { return output_layout_2.importPath; } });
60
64
  class Transpiler {
61
65
  options;
62
- plugin;
63
- constructor(options, plugin) {
66
+ constructor(options) {
64
67
  this.options = options;
65
- this.plugin = plugin;
66
68
  if (this.options === undefined) {
67
69
  this.options = {};
68
70
  }
@@ -76,23 +78,22 @@ class Transpiler {
76
78
  const reg = new abaplint.Registry().addFiles(memory);
77
79
  return this.run(reg);
78
80
  }
79
- async run(reg, progress) {
81
+ async run(reg, progress, folders) {
80
82
  // Validation installs the transpiler configuration and findIssues() parses
81
83
  // dirty registries. Parsing before that would be wasted because setConfig()
82
84
  // marks every registry object dirty again.
83
85
  this.validate(reg);
86
+ const layout = new output_layout_1.OutputLayout(reg, folders);
84
87
  const dbSetup = new db_1.DatabaseSetup(reg).run(this.options);
85
- this.plugin?.amendDatabaseSetup?.(dbSetup, reg, this.options || {});
86
88
  const output = {
87
89
  objects: [],
88
- unitTestScript: new unit_test_1.UnitTest().unitTestScript(reg, this.options?.skip),
89
- unitTestScriptOpen: new unit_test_1.UnitTest().unitTestScriptOpen(reg, this.options?.skip),
90
+ unitTestScript: new unit_test_1.UnitTest(layout).unitTestScript(reg, this.options?.skip),
91
+ unitTestScriptOpen: new unit_test_1.UnitTest(layout).unitTestScriptOpen(reg, this.options?.skip),
90
92
  initializationScript: "",
91
93
  initializationScript2: "",
92
94
  databaseSetup: dbSetup,
93
95
  reg: reg,
94
96
  };
95
- const pluginOutputs = [];
96
97
  progress?.set(reg.getObjectCount().total, "Building");
97
98
  for (const obj of reg.getObjects()) {
98
99
  await progress?.tick("Building, " + obj.getName());
@@ -109,7 +110,7 @@ class Transpiler {
109
110
  output.objects.push(...new handle_fugr_1.HandleFUGR(this.options).runObject(obj, reg));
110
111
  }
111
112
  else if (obj instanceof abaplint.ABAPObject) {
112
- output.objects.push(...new handle_abap_1.HandleABAP(this.options).runObject(obj, reg));
113
+ output.objects.push(...new handle_abap_1.HandleABAP(this.options, layout).runObject(obj, reg));
113
114
  }
114
115
  else if (obj instanceof abaplint.Objects.Oauth2Profile) {
115
116
  output.objects.push(...new handle_oa2p_1.HandleOA2P().runObject(obj, reg));
@@ -130,30 +131,25 @@ class Transpiler {
130
131
  output.objects.push(...new handle_table_type_1.HandleTableType().runObject(obj, reg));
131
132
  }
132
133
  else if (obj instanceof abaplint.Objects.MIMEObject) {
133
- output.objects.push(...new handle_smim_1.HandleSMIM().runObject(obj, reg));
134
+ output.objects.push(...new handle_smim_1.HandleSMIM(layout).runObject(obj, reg));
134
135
  }
135
136
  else if (obj instanceof abaplint.Objects.WebMIME) {
136
- output.objects.push(...new handle_w3mi_1.HandleW3MI().runObject(obj, reg));
137
+ output.objects.push(...new handle_w3mi_1.HandleW3MI(layout).runObject(obj, reg));
137
138
  }
138
139
  else if (obj instanceof abaplint.Objects.MessageClass) {
139
140
  output.objects.push(...new handle_msag_1.HandleMSAG().runObject(obj, reg));
140
141
  }
141
- else if (this.plugin !== undefined) {
142
- const handled = this.plugin.handleObject(obj, reg, this.options || {});
143
- if (handled !== undefined) {
144
- output.objects.push(...handled);
145
- pluginOutputs.push(...handled);
146
- }
147
- }
148
142
  }
149
- // after the object loop, plugin output files are imported by the initialization scripts
150
- output.initializationScript = new initialization_1.Initialization().script(reg, dbSetup, this.options, false, pluginOutputs);
151
- output.initializationScript2 = new initialization_1.Initialization().script(reg, dbSetup, this.options, true, pluginOutputs);
143
+ output.initializationScript = new initialization_1.Initialization(layout).script(reg, dbSetup, this.options, false);
144
+ output.initializationScript2 = new initialization_1.Initialization(layout).script(reg, dbSetup, this.options, true);
145
+ for (const file of output.objects) {
146
+ file.filename = layout.file(file.object, file.filename);
147
+ }
152
148
  return output;
153
149
  }
154
150
  // ///////////////////////////////
155
151
  validate(reg) {
156
- const issues = new validation_1.Validation(this.options, this.plugin).run(reg);
152
+ const issues = new validation_1.Validation(this.options).run(reg);
157
153
  if (issues.length > 0) {
158
154
  const messages = issues.map(i => i.getKey() + ", " +
159
155
  i.getMessage() + ", " +
@@ -1,10 +1,13 @@
1
1
  import { DatabaseSetupResult } from "./db/database_setup_result";
2
2
  import * as abaplint from "@abaplint/core";
3
- import { ITranspilerOptions, IOutputFile } from "./types";
3
+ import { ITranspilerOptions } from "./types";
4
+ import { OutputLayout } from "./output_layout";
4
5
  export declare function escapeNamespaceFilename(filename: string): string;
5
6
  export declare function escapeFilenameForImport(filename: string): string;
6
7
  export declare class Initialization {
7
- script(reg: abaplint.IRegistry, dbSetup: DatabaseSetupResult, options: ITranspilerOptions | undefined, useImport?: boolean, pluginOutputs?: readonly IOutputFile[]): string;
8
+ private readonly layout;
9
+ constructor(layout?: OutputLayout);
10
+ script(reg: abaplint.IRegistry, dbSetup: DatabaseSetupResult, options: ITranspilerOptions | undefined, useImport?: boolean): string;
8
11
  private buildImports;
9
12
  private hasClassConstructor;
10
13
  }
@@ -38,6 +38,7 @@ exports.escapeNamespaceFilename = escapeNamespaceFilename;
38
38
  exports.escapeFilenameForImport = escapeFilenameForImport;
39
39
  const abaplint = __importStar(require("@abaplint/core"));
40
40
  const handle_fugr_1 = require("./handlers/handle_fugr");
41
+ const output_layout_1 = require("./output_layout");
41
42
  function escapeNamespaceFilename(filename) {
42
43
  // ES modules are resolved and cached as URLs. This means that special characters must be
43
44
  // percent-encoded, such as # with %23 and ? with %3F.
@@ -55,7 +56,12 @@ function escapeFilenameForImport(filename) {
55
56
  return filename.replace(/%/g, "%25").replace(/#/g, "%23");
56
57
  }
57
58
  class Initialization {
58
- script(reg, dbSetup, options, useImport, pluginOutputs) {
59
+ layout;
60
+ constructor(layout = new output_layout_1.OutputLayout()) {
61
+ this.layout = layout;
62
+ // Flat output unless the caller supplies ownership information.
63
+ }
64
+ script(reg, dbSetup, options, useImport) {
59
65
  let ret = "";
60
66
  if (useImport === true) {
61
67
  ret = `/* eslint-disable import/newline-after-import */
@@ -98,28 +104,24 @@ globalThis.abap = new runtime.ABAP();\n`;
98
104
  }
99
105
  ret += `}\n\n`;
100
106
  ret += `await initializeABAP();\n\n`;
101
- ret += `${this.buildImports(reg, useImport, options, pluginOutputs)}`;
107
+ ret += `${this.buildImports(reg, useImport, options)}`;
102
108
  if (options?.setup?.postFunction !== undefined) {
103
109
  ret += `\n\nawait setup.${options?.setup?.postFunction}();\n`;
104
110
  }
105
111
  return ret;
106
112
  }
107
- buildImports(reg, useImport, options, pluginOutputs) {
113
+ buildImports(reg, useImport, options) {
108
114
  // note: ES modules are hoised, so use the dynamic import(), due to setting of globalThis.abap
109
115
  // some sorting required: eg. a class constructor using constant from interface
110
116
  const list = [];
111
117
  const late = [];
112
118
  const imp = (filename) => {
113
- if (useImport === true) {
114
- return `import "./${filename}.mjs";`;
115
- }
116
- else {
117
- return `await import("./${filename}.mjs");`;
118
- }
119
+ const specifier = (0, output_layout_1.importPath)("init.mjs", filename);
120
+ return {
121
+ filename: specifier.split("/").pop(),
122
+ code: useImport === true ? `import "${specifier}";` : `await import("${specifier}");`,
123
+ };
119
124
  };
120
- for (const pluginOutput of pluginOutputs || []) {
121
- list.push(imp(pluginOutput.filename.replace(/\.mjs$/i, "")));
122
- }
123
125
  for (const obj of reg.getObjects()) {
124
126
  if (obj instanceof abaplint.Objects.Table
125
127
  || obj instanceof abaplint.Objects.DataElement
@@ -131,11 +133,11 @@ globalThis.abap = new runtime.ABAP();\n`;
131
133
  || obj instanceof abaplint.Objects.TypePool
132
134
  || obj instanceof abaplint.Objects.TableType
133
135
  || obj instanceof abaplint.Objects.View) {
134
- list.push(imp(`${escapeNamespaceFilename(obj.getName().toLowerCase())}.${obj.getType().toLowerCase()}`));
136
+ list.push(imp(this.layout.objectModule(obj)));
135
137
  }
136
138
  }
137
139
  for (const obj of reg.getObjects()) {
138
- const name = imp(`${escapeNamespaceFilename(obj.getName().toLowerCase())}.${obj.getType().toLowerCase()}`);
140
+ const name = imp(this.layout.objectModule(obj));
139
141
  if (obj instanceof abaplint.Objects.Class
140
142
  && obj.getName().toUpperCase() !== "CL_ABAP_CHAR_UTILITIES"
141
143
  && this.hasClassConstructor(reg, obj)) {
@@ -155,7 +157,9 @@ globalThis.abap = new runtime.ABAP();\n`;
155
157
  list.push(name);
156
158
  }
157
159
  }
158
- return [...list.sort(), ...late].join("\n");
160
+ // Folder names must not change the original module initialization order.
161
+ list.sort((a, b) => a.filename < b.filename ? -1 : a.filename > b.filename ? 1 : 0);
162
+ return [...list, ...late].map(entry => entry.code).join("\n");
159
163
  }
160
164
  // class constructors might make early use of eg. constants from interfaces
161
165
  // sub classes will import() super classes and trigger a class constructor of the super
@@ -0,0 +1,17 @@
1
+ import * as abaplint from "@abaplint/core";
2
+ import { IObjectIdentifier } from "./types";
3
+ /** Source filenames to output folders. Omit this map to retain flat output. */
4
+ export type OutputFolders = ReadonlyMap<string, string>;
5
+ /** Browser-safe paths: generated modules use POSIX paths on every platform. */
6
+ export declare class OutputLayout {
7
+ private readonly grouped;
8
+ private readonly objectFolders;
9
+ private readonly sourceFolders;
10
+ constructor(reg?: abaplint.IRegistry, folders?: OutputFolders);
11
+ private key;
12
+ file(obj: IObjectIdentifier, filename: string): string;
13
+ sourceModule(filename: string): string;
14
+ objectModule(obj: abaplint.IObject, suffix?: string): string;
15
+ }
16
+ /** Escape each segment without turning directory separators into namespace markers. */
17
+ export declare function importPath(from: string, to: string): string;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OutputLayout = void 0;
4
+ exports.importPath = importPath;
5
+ /** Browser-safe paths: generated modules use POSIX paths on every platform. */
6
+ class OutputLayout {
7
+ grouped;
8
+ objectFolders = new Map();
9
+ sourceFolders = new Map();
10
+ constructor(reg, folders) {
11
+ this.grouped = folders !== undefined;
12
+ if (folders === undefined) {
13
+ return;
14
+ }
15
+ for (const [filename, folder] of folders) {
16
+ this.sourceFolders.set(filename.toLowerCase(), folder);
17
+ }
18
+ for (const obj of reg?.getObjects() || []) {
19
+ const owners = new Set(obj.getFiles().map(f => this.sourceFolders.get(f.getFilename().toLowerCase()))
20
+ .filter((folder) => folder !== undefined));
21
+ if (owners.size !== 1) {
22
+ throw new Error("Ambiguous output ownership for " + obj.getType() + " " + obj.getName());
23
+ }
24
+ const folder = [...owners][0];
25
+ this.objectFolders.set(this.key({ type: obj.getType(), name: obj.getName() }), folder);
26
+ for (const file of obj.getFiles()) {
27
+ // Class locals are merged into a single module.
28
+ this.sourceFolders.set(file.getFilename().toLowerCase().replace(/\.locals_(def|imp)\.abap$/, ".locals.abap"), folder);
29
+ }
30
+ }
31
+ }
32
+ key(obj) {
33
+ return obj.type.toUpperCase() + ":" + obj.name.toUpperCase();
34
+ }
35
+ file(obj, filename) {
36
+ const folder = this.objectFolders.get(this.key(obj));
37
+ if (this.grouped && folder === undefined) {
38
+ throw new Error("Missing output ownership for " + obj.type + " " + obj.name);
39
+ }
40
+ if (folder !== undefined && (filename.startsWith("/") || /[\\:]/.test(filename)
41
+ || filename.split("/").some(segment => segment === "" || segment === "." || segment === ".."))) {
42
+ throw new Error("Invalid output filename: " + filename);
43
+ }
44
+ return folder === undefined ? filename : folder + "/" + filename;
45
+ }
46
+ sourceModule(filename) {
47
+ const folder = this.sourceFolders.get(filename.toLowerCase());
48
+ if (this.grouped && folder === undefined) {
49
+ throw new Error("Missing output ownership for source " + filename);
50
+ }
51
+ const module = filename.replace(/\.abap$/i, ".mjs").toLowerCase();
52
+ return folder === undefined ? module : folder + "/" + module;
53
+ }
54
+ objectModule(obj, suffix = "") {
55
+ const identifier = { name: obj.getName(), type: obj.getType() };
56
+ const filename = obj.getName().toLowerCase().replace(/\//g, "#") + "." + obj.getType().toLowerCase() + suffix + ".mjs";
57
+ return this.file(identifier, filename);
58
+ }
59
+ }
60
+ exports.OutputLayout = OutputLayout;
61
+ /** Escape each segment without turning directory separators into namespace markers. */
62
+ function importPath(from, to) {
63
+ const source = from.split("/").slice(0, -1);
64
+ const target = to.split("/");
65
+ while (source.length > 0 && target.length > 0 && source[0] === target[0]) {
66
+ source.shift();
67
+ target.shift();
68
+ }
69
+ const relative = [...source.map(() => ".."), ...target].join("/");
70
+ const escaped = relative.split("/").map(segment => encodeURIComponent(segment)).join("/");
71
+ return escaped.startsWith("../") ? escaped : "./" + escaped;
72
+ }
73
+ //# sourceMappingURL=output_layout.js.map
@@ -4,5 +4,8 @@ import { Traversal } from "../traversal";
4
4
  import { Chunk } from "../chunk";
5
5
  export declare class AssignTranspiler implements IStatementTranspiler {
6
6
  private tableExpressionSafeSource;
7
+ /** ASSIGN of a statically specified data object, e.g. a variable, an attribute or an offset/length,
8
+ * does not set sy-subrc, a dereference, a table expression or COMPONENT does */
9
+ private isStaticDataObject;
7
10
  transpile(node: abaplint.Nodes.StatementNode, traversal: Traversal): Chunk;
8
11
  }
@@ -55,6 +55,22 @@ throw error;
55
55
  }
56
56
  })())`;
57
57
  }
58
+ /** ASSIGN of a statically specified data object, e.g. a variable, an attribute or an offset/length,
59
+ * does not set sy-subrc, a dereference, a table expression or COMPONENT does */
60
+ isStaticDataObject(node, source) {
61
+ const concat = node.concatTokens().toUpperCase();
62
+ if (source === undefined
63
+ || concat.startsWith("ASSIGN COMPONENT ")
64
+ || concat.includes(" INCREMENT ")
65
+ || concat.includes(" ELSE UNASSIGN")
66
+ || source.findFirstExpression(abaplint.Expressions.Dereference) !== undefined
67
+ || source.findFirstExpression(abaplint.Expressions.TableExpression) !== undefined) {
68
+ return false;
69
+ }
70
+ // field symbols are left as they are, not measured
71
+ const first = source.findFirstExpression(abaplint.Expressions.FieldChain)?.getFirstChild();
72
+ return first !== undefined && !(first.get() instanceof abaplint.Expressions.SourceFieldSymbol);
73
+ }
58
74
  transpile(node, traversal) {
59
75
  const assignSource = node.findDirectExpression(abaplint.Expressions.AssignSource);
60
76
  const sourceExpressions = assignSource?.findDirectExpressionsMulti([abaplint.Expressions.Source, abaplint.Expressions.SimpleSource3]) || [];
@@ -94,6 +110,9 @@ throw error;
94
110
  }
95
111
  else {
96
112
  options.push("source: " + sources.pop());
113
+ if (this.isStaticDataObject(node, sourceExprForCheck)) {
114
+ options.push("keepSubrc: true");
115
+ }
97
116
  }
98
117
  }
99
118
  else {
@@ -57,7 +57,7 @@ static INTERNAL_TYPE = 'CLAS';
57
57
  static INTERNAL_NAME = '${traversal.buildInternalName(token.getStr(), def)}';
58
58
  static IMPLEMENTED_INTERFACES = [${this.findImplementedByClass(traversal, def, scope).map(e => `"` + e.toUpperCase() + `"`).join(",")}];
59
59
  static ATTRIBUTES = {${Array.from(traversal.buildAttributes(def, scope)).join(",\n")}};
60
- static METHODS = {${traversal.buildMethods(def).join(",\n")}};`, node, traversal);
60
+ static METHODS = {${traversal.buildMethods(def, scope).join(",\n")}};`, node, traversal);
61
61
  }
62
62
  findImplementedInterface(traversal, def, scope) {
63
63
  if (def === undefined || scope === undefined) {
@@ -100,6 +100,16 @@ class MethodImplementationTranspiler {
100
100
  else {
101
101
  after += `let ${varPrefixed} = ${unique}?.${varName};\n`;
102
102
  }
103
+ if (passByValue === true) {
104
+ // pass by value: the method works on its own copy, also for a generic type,
105
+ // which keeps the type of the actual parameter.
106
+ // A field symbol that is unassigned or assigned with CASTING is passed on as it is
107
+ after += `if (${varPrefixed} instanceof abap.types.FieldSymbol) {
108
+ if (${varPrefixed}.getPointer()?.clone !== undefined) { ${varPrefixed} = ${varPrefixed}.getPointer().clone(); }
109
+ } else if (${unique}?.${varName} !== undefined && ${varPrefixed}?.clone !== undefined) {
110
+ ${varPrefixed} = ${varPrefixed}.clone();
111
+ }\n`;
112
+ }
103
113
  if (type instanceof abaplint.BasicTypes.NumericGenericType) {
104
114
  after += `if (${varPrefixed}.constructor.name === "Character") {
105
115
  ${varPrefixed} = ${transpile_types_1.TranspileTypes.toType(identifier.getType())};
@@ -296,10 +296,10 @@ abap.builtin.sy.get().dbcnt.set(${target}.getArrayLength());
296
296
  + ` : (("" + ${code}.get()).trim() === "" ? "" : "${keyword} " + ${code}.get()))`;
297
297
  }
298
298
  dynamicHavingClause(code) {
299
- return `((value) => {
300
- const condition = value instanceof abap.types.Table || value instanceof abap.types.HashedTable
301
- ? value.array().map(row => row.get()).join(" ") : value.get();
302
- return condition.trim() === "" ? "" : "HAVING " + abap.expandDynamic(condition, (name) => {try { return eval(name);} catch {}});
299
+ return `((value) => {
300
+ const condition = value instanceof abap.types.Table || value instanceof abap.types.HashedTable
301
+ ? value.array().map(row => row.get()).join(" ") : value.get();
302
+ return condition.trim() === "" ? "" : "HAVING " + abap.expandDynamic(condition, (name) => {try { return eval(name);} catch {}});
303
303
  })(${code})`;
304
304
  }
305
305
  isWhereExpression(node, expression) {
@@ -54,8 +54,11 @@ class InterfaceTranspiler {
54
54
  ret += `static INTERNAL_TYPE = 'INTF';\n`;
55
55
  ret += `static INTERNAL_NAME = '${traversal.buildInternalName(token.getStr(), def)}';\n`;
56
56
  ret += `static ATTRIBUTES = {${Array.from(traversal.buildAttributes(def, scope)).join(",\n")}};\n`;
57
- ret += `static METHODS = {${traversal.buildMethods(def).join(",\n")}};\n`;
58
- // todo, add IMPLEMENTED_INTERFACES ?
57
+ ret += `static METHODS = {${traversal.buildMethods(def, scope).join(",\n")}};\n`;
58
+ const interfaces = def?.getImplementing().map(i => `"${i.name.toUpperCase()}"`) || [];
59
+ if (interfaces.length > 0) {
60
+ ret += `static IMPLEMENTED_INTERFACES = [${interfaces.join(",")}];\n`;
61
+ }
59
62
  }
60
63
  else if (c instanceof abaplint.Nodes.StatementNode && c.get() instanceof abaplint.Statements.EndInterface) {
61
64
  ret += "}\n";
@@ -68,8 +68,22 @@ class TranspileTypes {
68
68
  if (type instanceof abaplint.BasicTypes.ObjectReferenceType
69
69
  || type instanceof abaplint.BasicTypes.GenericObjectReferenceType) {
70
70
  resolved = "ABAPObject";
71
- extra = "{qualifiedName: " + JSON.stringify(type.getQualifiedName()?.toUpperCase()) +
72
- ", RTTIName: " + JSON.stringify(type.getRTTIName()?.toUpperCase()) + "}";
71
+ const qualifiedName = type.getQualifiedName()
72
+ ?? (type instanceof abaplint.BasicTypes.ObjectReferenceType ? type.getIdentifierName() : undefined);
73
+ let RTTIName = type.getRTTIName();
74
+ if (type instanceof abaplint.BasicTypes.ObjectReferenceType && RTTIName === undefined) {
75
+ const id = type.getIdentifier();
76
+ // NEW and CAST can omit metadata; recover it from the referenced definition.
77
+ const [name, kind] = id.getFilename().split("/").pop().replace(/#/g, "/").split(".");
78
+ const local = (id instanceof abaplint.Types.ClassDefinition || id instanceof abaplint.Types.InterfaceDefinition)
79
+ && id.isGlobal() === false;
80
+ const prefix = local && kind === "prog" ? "\\PROGRAM=" + name
81
+ : local && kind === "clas" ? "\\CLASS-POOL=" + name : "";
82
+ const category = id instanceof abaplint.Types.InterfaceDefinition || kind === "intf" ? "INTERFACE" : "CLASS";
83
+ RTTIName = prefix + "\\" + category + "=" + id.getName();
84
+ }
85
+ extra = "{qualifiedName: " + JSON.stringify(qualifiedName?.toUpperCase()) +
86
+ ", RTTIName: " + JSON.stringify(RTTIName?.toUpperCase()) + "}";
73
87
  }
74
88
  else if (type instanceof abaplint.BasicTypes.TableType) {
75
89
  resolved = "Table";
@@ -197,6 +211,11 @@ class TranspileTypes {
197
211
  resolved = "Packed";
198
212
  extra = "{length: 8, decimals: 2}";
199
213
  }
214
+ else if (type instanceof abaplint.BasicTypes.PGenericType) {
215
+ // if not supplied its a P LENGTH 8 DECIMALS 0
216
+ resolved = "Packed";
217
+ extra = "{length: 8, decimals: 0}";
218
+ }
200
219
  else if (type instanceof abaplint.BasicTypes.XStringType) {
201
220
  resolved = "XString";
202
221
  if (type.getQualifiedName() !== undefined) {
@@ -49,7 +49,8 @@ export declare class Traversal {
49
49
  private isClassAttribute;
50
50
  prefixAndName(t: abaplint.Token, filename?: string): string;
51
51
  private isStaticClassAttribute;
52
- buildMethods(def: abaplint.IClassDefinition | abaplint.IInterfaceDefinition | undefined): string[];
52
+ buildMethods(def: abaplint.IClassDefinition | abaplint.IInterfaceDefinition | undefined, scope?: abaplint.ISpaghettiScopeNode): string[];
53
+ private buildMethod;
53
54
  private mapVisibility;
54
55
  buildAttributes(def: abaplint.IClassDefinition | abaplint.IInterfaceDefinition | undefined, scope: abaplint.ISpaghettiScopeNode | undefined, prefix?: string): Set<string>;
55
56
  isBuiltinMethod(token: abaplint.Token): boolean;
@@ -355,7 +355,7 @@ class Traversal {
355
355
  }
356
356
  return undefined;
357
357
  }
358
- buildMethods(def) {
358
+ buildMethods(def, scope) {
359
359
  const methods = [];
360
360
  if (def === undefined) {
361
361
  return methods;
@@ -366,29 +366,57 @@ class Traversal {
366
366
  throw new Error("buildMethods: unexpected undefined, " + def.getName());
367
367
  }
368
368
  for (const m of methodDefinitions.getAll()) {
369
- const parameters = [];
370
- for (const p of m.getParameters().getAll()) {
371
- const type = transpile_types_1.TranspileTypes.toType(p.getType());
372
- const optional = m.getParameters().getOptional().includes(p.getName()) ? "X" : " ";
373
- let pKind = "";
374
- if (pKind === "") {
375
- pKind = m.getParameters().getImporting().find((t) => { return t.getName() === p.getName(); }) ? "I" : "";
376
- }
377
- if (pKind === "") {
378
- pKind = m.getParameters().getChanging().find((t) => { return t.getName() === p.getName(); }) ? "C" : "";
379
- }
380
- if (pKind === "") {
381
- pKind = m.getParameters().getExporting().find((t) => { return t.getName() === p.getName(); }) ? "E" : "";
382
- }
383
- if (pKind === "") {
384
- pKind = m.getParameters().getReturning()?.getName() === p.getName() ? "R" : "";
385
- }
386
- parameters.push(`"${p.getName().toUpperCase()}": {"type": () => {return ${type};}, "is_optional": "${optional}", "parm_kind": "${pKind}", "type_name": "${p.getType().constructor.name}"}`);
369
+ methods.push(this.buildMethod(m.getName(), m, m.getVisibility()));
370
+ }
371
+ // an alias for an interface method is listed under its own name, pointing to the method
372
+ for (const a of def.getAliases() || []) {
373
+ const [intfName, methodName] = a.getComponent().split("~");
374
+ if (methodName === undefined) {
375
+ continue;
376
+ }
377
+ const aliased = this.findInterfaceDefinition(intfName, scope)?.getMethodDefinitions()?.getByName(methodName);
378
+ if (aliased === undefined) {
379
+ // not a method, or the interface is unknown
380
+ continue;
387
381
  }
388
- methods.push(`"${m.getName().toUpperCase()}": {"visibility": "${this.mapVisibility(m.getVisibility())}", "parameters": {${parameters.join(", ")}}}`);
382
+ methods.push(this.buildMethod(a.getName(), aliased, a.getVisibility(), a.getComponent()));
389
383
  }
390
384
  return methods;
391
385
  }
386
+ buildMethod(name, m, visibility, aliasFor) {
387
+ const optionalNames = m.getParameters().getOptional().map(o => o.toUpperCase());
388
+ const parameters = [];
389
+ for (const p of m.getParameters().getAll()) {
390
+ const type = transpile_types_1.TranspileTypes.toType(p.getType());
391
+ const optional = optionalNames.includes(p.getName().toUpperCase()) ? "X" : " ";
392
+ let pKind = "";
393
+ if (pKind === "") {
394
+ pKind = m.getParameters().getImporting().find((t) => { return t.getName() === p.getName(); }) ? "I" : "";
395
+ }
396
+ if (pKind === "") {
397
+ pKind = m.getParameters().getChanging().find((t) => { return t.getName() === p.getName(); }) ? "C" : "";
398
+ }
399
+ if (pKind === "") {
400
+ pKind = m.getParameters().getExporting().find((t) => { return t.getName() === p.getName(); }) ? "E" : "";
401
+ }
402
+ if (pKind === "") {
403
+ pKind = m.getParameters().getReturning()?.getName() === p.getName() ? "R" : "";
404
+ }
405
+ parameters.push(`"${p.getName().toUpperCase()}": {"type": () => {return ${type};}, "is_optional": "${optional}", "parm_kind": "${pKind}", "type_name": "${p.getType().constructor.name}"}`);
406
+ }
407
+ // only written when set, a method without them is described as before
408
+ let extra = "";
409
+ if (m.isStatic() === true) {
410
+ extra += `"is_class": "X", `;
411
+ }
412
+ if (m.getRaising().length > 0) {
413
+ extra += `"exceptions": [${m.getRaising().map(e => `"${e.toUpperCase()}"`).join(", ")}], `;
414
+ }
415
+ if (aliasFor !== undefined) {
416
+ extra += `"alias_for": "${aliasFor.toUpperCase()}", `;
417
+ }
418
+ return `"${name.toUpperCase()}": {"visibility": "${this.mapVisibility(visibility)}", ${extra}"parameters": {${parameters.join(", ")}}}`;
419
+ }
392
420
  mapVisibility(vis) {
393
421
  switch (vis) {
394
422
  case abaplint.Visibility.Private:
@@ -31,6 +31,7 @@ export interface IProgress {
31
31
  /** one javascript output file for each object */
32
32
  export interface IOutputFile {
33
33
  object: IObjectIdentifier;
34
+ /** POSIX path relative to the output root; includes its folder when a layout is supplied. */
34
35
  filename: string;
35
36
  chunk: Chunk;
36
37
  requires: readonly IRequire[];
@@ -40,18 +41,6 @@ export declare enum UnknownTypesEnum {
40
41
  compileError = "compileError",
41
42
  runtimeError = "runtimeError"
42
43
  }
43
- /** handles additional object types, may be supplied by a separate npm package */
44
- export interface ITranspilerPlugin {
45
- /** object types handled by the plugin, merged into allowed_object_types during validation */
46
- objectTypes(): string[];
47
- /** returns undefined if the object is not handled by the plugin,
48
- * returned output files are imported in the initialization script,
49
- * return an empty array to accept the object without producing output */
50
- handleObject(obj: abaplint.IObject, reg: abaplint.IRegistry, options: ITranspilerOptions): IOutputFile[] | undefined;
51
- /** optional, called after the default schemas and inserts are built,
52
- * amend the database setup by mutating the supplied result directly */
53
- amendDatabaseSetup?(dbSetup: DatabaseSetupResult, reg: abaplint.IRegistry, options: ITranspilerOptions): void;
54
- }
55
44
  export interface ITranspilerOptions {
56
45
  /** ignore syntax check, used for internal testing */
57
46
  ignoreSyntaxCheck?: boolean;
@@ -1,4 +1,5 @@
1
1
  import * as abaplint from "@abaplint/core";
2
+ import { OutputLayout } from "./output_layout";
2
3
  /** @uniqueItems true */
3
4
  export type TestMethodList = {
4
5
  /** @pattern ^[a-zA-Z0-9_\/]+$ */
@@ -10,6 +11,8 @@ export type TestMethodList = {
10
11
  note?: string;
11
12
  }[];
12
13
  export declare class UnitTest {
14
+ private readonly layout;
15
+ constructor(layout?: OutputLayout);
13
16
  unitTestScriptOpen(reg: abaplint.IRegistry, _skip?: TestMethodList): string;
14
17
  private getSortedTests;
15
18
  unitTestScript(reg: abaplint.IRegistry, skip?: TestMethodList): string;
@@ -36,8 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.UnitTest = void 0;
37
37
  /* eslint-disable max-len */
38
38
  const abaplint = __importStar(require("@abaplint/core"));
39
- const initialization_1 = require("./initialization");
39
+ const output_layout_1 = require("./output_layout");
40
40
  class UnitTest {
41
+ layout;
42
+ constructor(layout = new output_layout_1.OutputLayout()) {
43
+ this.layout = layout;
44
+ // Test runners live at the output root.
45
+ }
41
46
  unitTestScriptOpen(reg, _skip) {
42
47
  let ret = `/* eslint-disable curly */
43
48
  import fs from "fs";
@@ -58,7 +63,7 @@ async function run() {
58
63
  }
59
64
  const hasTestFile = obj.getFiles().some(f => { return f.getFilename().includes(".testclasses."); });
60
65
  if (hasTestFile === true) {
61
- ret += ` await import("./${(0, initialization_1.escapeNamespaceFilename)(obj.getName().toLowerCase())}.${obj.getType().toLowerCase()}.testclasses.mjs");\n`;
66
+ ret += ` await import("${(0, output_layout_1.importPath)("index.mjs", this.layout.objectModule(obj, ".testclasses"))}");\n`;
62
67
  }
63
68
  for (const file of obj.getABAPFiles()) {
64
69
  for (const def of file.getInfo().listClassDefinitions()) {
@@ -120,7 +125,7 @@ run().then(() => {
120
125
  }
121
126
  tests.push({
122
127
  obj,
123
- filename: `./${(0, initialization_1.escapeNamespaceFilename)(obj.getName().toLowerCase())}.${obj.getType().toLowerCase()}.testclasses.mjs`,
128
+ filename: `${(0, output_layout_1.importPath)("index.mjs", this.layout.objectModule(obj, ".testclasses"))}`,
124
129
  localClass: def.name.toLowerCase(),
125
130
  riskLevel: def.riskLevel,
126
131
  duration: def.duration,
@@ -1,9 +1,8 @@
1
1
  import { Issue, IRegistry, IConfig } from "@abaplint/core";
2
- import { ITranspilerOptions, ITranspilerPlugin } from "./types";
2
+ import { ITranspilerOptions } from "./types";
3
3
  export declare const config: IConfig;
4
4
  export declare class Validation {
5
5
  private readonly options;
6
- private readonly plugin;
7
- constructor(options?: ITranspilerOptions, plugin?: ITranspilerPlugin);
6
+ constructor(options?: ITranspilerOptions);
8
7
  run(reg: IRegistry): readonly Issue[];
9
8
  }
@@ -5,8 +5,12 @@ const core_1 = require("@abaplint/core");
5
5
  const types_1 = require("./types");
6
6
  const defaultAllowedObjectTypes = [
7
7
  "APIS",
8
+ "APLO",
8
9
  "AUTH",
10
+ "BDEF",
9
11
  "CLAS",
12
+ "DDLS",
13
+ "DDLX",
10
14
  "DEVC",
11
15
  "DOMA",
12
16
  "DTEL",
@@ -28,13 +32,19 @@ const defaultAllowedObjectTypes = [
28
32
  "PARA",
29
33
  "PINF",
30
34
  "PROG",
35
+ "SAJC",
36
+ "SAJT",
37
+ "SCO2",
31
38
  "WAPA",
32
39
  "SHLP",
33
40
  "SHMA",
41
+ "SIA6",
34
42
  "SICF",
35
43
  "SMIM",
36
44
  "SPLO",
37
45
  "SRFC",
46
+ "SRVB",
47
+ "SRVD",
38
48
  "SUSC",
39
49
  "SUSH",
40
50
  "SUSO",
@@ -110,10 +120,8 @@ exports.config = {
110
120
  // hmm this ^ is okay? since lines will be prefixed with "abap.builtin"?
111
121
  class Validation {
112
122
  options;
113
- plugin;
114
- constructor(options, plugin) {
123
+ constructor(options) {
115
124
  this.options = options;
116
- this.plugin = plugin;
117
125
  }
118
126
  run(reg) {
119
127
  if (this.options?.ignoreSyntaxCheck === true) {
@@ -123,13 +131,6 @@ class Validation {
123
131
  exports.config.rules["check_syntax"] = true;
124
132
  }
125
133
  exports.config.rules["forbidden_identifier"]["check"] = ["^unique\\d+$"];
126
- const allowed = [...defaultAllowedObjectTypes];
127
- for (const type of this.plugin?.objectTypes() || []) {
128
- if (allowed.includes(type.toUpperCase()) === false) {
129
- allowed.push(type.toUpperCase());
130
- }
131
- }
132
- exports.config.rules["allowed_object_types"]["allowed"] = allowed;
133
134
  if (this.options?.unknownTypes === types_1.UnknownTypesEnum.runtimeError) {
134
135
  // this is not a constant, just a regex that happens to not match anything
135
136
  exports.config.syntax.errorNamespace = "VOID_EVERYTHING";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abaplint/transpiler",
3
- "version": "2.13.100",
3
+ "version": "2.14.0",
4
4
  "description": "Transpiler",
5
5
  "main": "build/src/index.js",
6
6
  "typings": "build/src/index.d.ts",
@@ -29,7 +29,7 @@
29
29
  "author": "abaplint",
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "@abaplint/core": "^2.120.69",
32
+ "@abaplint/core": "^2.120.71",
33
33
  "source-map": "^0.8.0"
34
34
  },
35
35
  "devDependencies": {