@abaplint/transpiler 2.13.56 → 2.13.57

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.
@@ -1,7 +1,15 @@
1
- import { Nodes } from "@abaplint/core";
1
+ import { Nodes, Types } from "@abaplint/core";
2
2
  import { IExpressionTranspiler } from "./_expression_transpiler";
3
3
  import { Traversal } from "../traversal";
4
4
  import { Chunk } from "../chunk";
5
5
  export declare class MethodCallTranspiler implements IExpressionTranspiler {
6
+ private readonly postName;
7
+ private readonly method;
8
+ /** @param postName inserted between the method name and the parameters, eg. ".bind(this)"
9
+ * @param method the already resolved method reference, saves looking it up again */
10
+ constructor(postName?: string, method?: {
11
+ def: Types.MethodDefinition;
12
+ name: string;
13
+ });
6
14
  transpile(node: Nodes.ExpressionNode, traversal: Traversal): Chunk;
7
15
  }
@@ -6,6 +6,14 @@ const traversal_1 = require("../traversal");
6
6
  const method_call_param_1 = require("./method_call_param");
7
7
  const chunk_1 = require("../chunk");
8
8
  class MethodCallTranspiler {
9
+ postName;
10
+ method;
11
+ /** @param postName inserted between the method name and the parameters, eg. ".bind(this)"
12
+ * @param method the already resolved method reference, saves looking it up again */
13
+ constructor(postName = "", method) {
14
+ this.postName = postName;
15
+ this.method = method;
16
+ }
9
17
  transpile(node, traversal) {
10
18
  let post = "";
11
19
  const nameToken = node.findDirectExpression(core_1.Expressions.MethodName)?.getFirstToken();
@@ -13,7 +21,7 @@ class MethodCallTranspiler {
13
21
  throw new Error("MethodCallTranspiler, name not found");
14
22
  }
15
23
  const scope = traversal.findCurrentScopeByToken(nameToken);
16
- const m = traversal.findMethodReference(nameToken, scope);
24
+ const m = this.method ?? traversal.findMethodReference(nameToken, scope);
17
25
  let name = nameToken.getStr().toLowerCase();
18
26
  if (traversal.isBuiltinMethod(nameToken)) {
19
27
  // todo: this is not correct, the method name might be shadowed
@@ -38,12 +46,12 @@ class MethodCallTranspiler {
38
46
  name = `FRIENDS_ACCESS_INSTANCE["${name}"]`;
39
47
  }
40
48
  }
41
- name = name + "(";
49
+ name = name + this.postName + "(";
42
50
  }
43
51
  else {
44
52
  // todo: this should never happen?
45
53
  name = traversal_1.Traversal.escapeNamespace(name.replace("~", "$"));
46
- name = name + "(";
54
+ name = name + this.postName + "(";
47
55
  }
48
56
  const step = node.findDirectExpression(core_1.Expressions.MethodCallParam);
49
57
  if (step === undefined) {
@@ -4,22 +4,46 @@ exports.MethodCallChainTranspiler = void 0;
4
4
  const core_1 = require("@abaplint/core");
5
5
  const traversal_1 = require("../traversal");
6
6
  const chunk_1 = require("../chunk");
7
+ const method_call_1 = require("./method_call");
7
8
  class MethodCallChainTranspiler {
8
9
  transpile(node, traversal) {
9
10
  let ret = new chunk_1.Chunk();
10
- for (const c of node.getChildren()) {
11
+ const children = node.getChildren();
12
+ for (const c of children) {
11
13
  if (c instanceof core_1.Nodes.ExpressionNode && c.get() instanceof core_1.Expressions.MethodCall) {
12
- const sub = traversal.traverse(c);
14
+ const isFirst = c === node.getFirstChild();
15
+ // "me->name( )" has the same semantics as the unqualified "name( )",
16
+ // only the first call in the chain is on "me", the rest is dispatched dynamically
17
+ const viaMe = c === children[2] && traversal_1.Traversal.isMe(children[0]);
18
+ let prefix = undefined;
19
+ let method = undefined;
20
+ const nameToken = isFirst || viaMe
21
+ ? c.findDirectExpression(core_1.Expressions.MethodName)?.getFirstToken()
22
+ : undefined;
23
+ // resolving the method reference is a linear scan of the scope, only do it when it can matter
24
+ const enclosingClass = nameToken ? traversal.enclosingConstructorClass(nameToken) : undefined;
25
+ if (nameToken && enclosingClass !== undefined) {
26
+ method = traversal.findMethodReference(nameToken, traversal.findCurrentScopeByToken(nameToken));
27
+ prefix = traversal.constructorPrototypePrefix(nameToken, method?.def, enclosingClass);
28
+ }
29
+ const sub = prefix === undefined
30
+ ? traversal.traverse(c)
31
+ : new method_call_1.MethodCallTranspiler(".bind(this)", method).transpile(c, traversal);
13
32
  if (sub.getCode().startsWith("abap.builtin.")
14
33
  || sub.getCode().startsWith("await abap.builtin.")) {
15
34
  ret.appendChunk(sub);
16
35
  }
17
36
  else {
18
- const t = c === node.getFirstChild() ? "this." : "";
37
+ let receiver = ret;
38
+ let t = isFirst ? "this." : "";
39
+ if (prefix !== undefined) {
40
+ t = prefix;
41
+ receiver = new chunk_1.Chunk(); // discard the "this.me.get()." built for the viaMe case
42
+ }
19
43
  ret = new chunk_1.Chunk()
20
44
  .appendString("(await ")
21
45
  .append(t, node, traversal)
22
- .appendChunk(ret)
46
+ .appendChunk(receiver)
23
47
  .appendChunk(sub)
24
48
  .appendString(")");
25
49
  }
@@ -5,6 +5,9 @@ import { Chunk } from "../chunk";
5
5
  export declare class MethodSourceTranspiler implements IExpressionTranspiler {
6
6
  private prepend;
7
7
  private readonly privatePrefix;
8
- constructor(prepend?: string, privatePrefix?: boolean);
8
+ private readonly staticConstructorCall;
9
+ /** @param staticConstructorCall set to false when building a reference to the method instead of calling it,
10
+ * eg. SET HANDLER, where the dynamic method is the one to be registered */
11
+ constructor(prepend?: string, privatePrefix?: boolean, staticConstructorCall?: boolean);
9
12
  transpile(node: Nodes.ExpressionNode, traversal: Traversal): Chunk;
10
13
  }
@@ -9,14 +9,19 @@ const _1 = require(".");
9
9
  class MethodSourceTranspiler {
10
10
  prepend;
11
11
  privatePrefix;
12
- constructor(prepend, privatePrefix = false) {
12
+ staticConstructorCall;
13
+ /** @param staticConstructorCall set to false when building a reference to the method instead of calling it,
14
+ * eg. SET HANDLER, where the dynamic method is the one to be registered */
15
+ constructor(prepend, privatePrefix = false, staticConstructorCall = true) {
13
16
  this.prepend = (prepend || "") + "await ";
14
17
  this.privatePrefix = privatePrefix;
18
+ this.staticConstructorCall = staticConstructorCall;
15
19
  }
16
20
  transpile(node, traversal) {
17
21
  const ret = new chunk_1.Chunk();
18
22
  const children = node.getChildren();
19
23
  let call = "";
24
+ let bindConstructorCall = false;
20
25
  for (let i = 0; i < children.length; i++) {
21
26
  const child = children[i];
22
27
  const nextChild = children[i + 1];
@@ -78,11 +83,25 @@ class MethodSourceTranspiler {
78
83
  }
79
84
  else if (child.get() instanceof core_1.Expressions.MethodName
80
85
  || child.get() instanceof core_1.Expressions.AttributeName) {
81
- if (i === 0) {
86
+ const nameToken = child.getFirstToken();
87
+ const scope = traversal.findCurrentScopeByToken(nameToken);
88
+ const m = traversal.findMethodReference(nameToken, scope);
89
+ // "me->name( )" has the same semantics as the unqualified "name( )"
90
+ const viaMe = i === 2 && children.length === 3 && traversal_1.Traversal.isMe(children[0]);
91
+ if (this.staticConstructorCall && (i === 0 || viaMe)) {
92
+ const prefix = traversal.constructorPrototypePrefix(nameToken, m?.def);
93
+ if (prefix !== undefined) {
94
+ this.prepend += prefix;
95
+ call = ""; // discard the "this.me.get()." built for the viaMe case
96
+ bindConstructorCall = true;
97
+ }
98
+ else if (i === 0) {
99
+ this.prepend += "this.";
100
+ }
101
+ }
102
+ else if (i === 0) {
82
103
  this.prepend += "this.";
83
104
  }
84
- const nameToken = child.getFirstToken();
85
- const m = traversal.findMethodReference(nameToken, traversal.findCurrentScopeByToken(nameToken));
86
105
  if (m) {
87
106
  call += traversal_1.Traversal.escapeNamespace(m.name.toLowerCase().replace("~", "$"));
88
107
  }
@@ -105,9 +124,19 @@ class MethodSourceTranspiler {
105
124
  else if (child.get() instanceof core_1.Expressions.FieldChain
106
125
  || child.get() instanceof core_1.Expressions.SourceField) {
107
126
  const nameToken = child.getFirstToken();
108
- const m = traversal.findMethodReference(nameToken, traversal.findCurrentScopeByToken(nameToken));
127
+ const scope = traversal.findCurrentScopeByToken(nameToken);
128
+ const m = traversal.findMethodReference(nameToken, scope);
109
129
  if (i === 0 && m) {
110
- this.prepend += "this.";
130
+ const prefix = this.staticConstructorCall
131
+ ? traversal.constructorPrototypePrefix(nameToken, m.def)
132
+ : undefined;
133
+ if (prefix !== undefined) {
134
+ this.prepend += prefix;
135
+ bindConstructorCall = true;
136
+ }
137
+ else {
138
+ this.prepend += "this.";
139
+ }
111
140
  if (this.privatePrefix && m.def.getVisibility() === core_1.Visibility.Private
112
141
  && m.def.isStatic() === false) { // todo: this is probably wrong?
113
142
  this.prepend += "#";
@@ -128,6 +157,9 @@ class MethodSourceTranspiler {
128
157
  ret.appendString("MethodSourceTranspiler-" + child.get().constructor.name + "-todo");
129
158
  }
130
159
  }
160
+ if (bindConstructorCall) {
161
+ call += ".bind(this)";
162
+ }
131
163
  ret.appendString(this.prepend);
132
164
  ret.appendString(call);
133
165
  return ret;
@@ -43,7 +43,9 @@ class SetHandlerTranspiler {
43
43
  let eventClass = undefined;
44
44
  let eventName = undefined;
45
45
  for (const m of node.findDirectExpressions(abaplint.Expressions.MethodSource)) {
46
- const msource = new expressions_1.MethodSourceTranspiler("", true).transpile(m, traversal).getCode().replace("await ", "");
46
+ // the handler method is registered as a reference, it is dispatched dynamically when the event is raised,
47
+ // so no static binding even if the SET HANDLER is inside a constructor
48
+ const msource = new expressions_1.MethodSourceTranspiler("", true, false).transpile(m, traversal).getCode().replace("await ", "");
47
49
  const lastDot = msource.lastIndexOf(".");
48
50
  const receiver = lastDot === -1 ? "this" : msource.substring(0, lastDot);
49
51
  methods.push(`{method: ${msource}, receiver: ${receiver}}`);
@@ -11,6 +11,8 @@ export declare class Traversal {
11
11
  readonly options: ITranspilerOptions | undefined;
12
12
  constructor(spaghetti: abaplint.ISpaghettiScope, file: abaplint.ABAPFile, obj: abaplint.ABAPObject, reg: abaplint.IRegistry, options?: ITranspilerOptions);
13
13
  static escapeNamespace(name: string | undefined): string | undefined;
14
+ /** the self reference "me", it is a FieldChain in eg. MethodCallChain and a SourceField in eg. MethodSource */
15
+ static isMe(node: abaplint.Nodes.ExpressionNode | abaplint.Nodes.TokenNode | abaplint.Nodes.StructureNode): boolean;
14
16
  static prefixVariable(name: string | undefined): string;
15
17
  getCurrentObject(): abaplint.ABAPObject;
16
18
  traverse(node: abaplint.INode | undefined): Chunk;
@@ -39,6 +41,27 @@ export declare class Traversal {
39
41
  def: abaplint.Types.MethodDefinition;
40
42
  name: string;
41
43
  };
44
+ /** Returns the name of the class implementing the instance constructor the token is located in,
45
+ * or undefined if the token is not inside an instance constructor.
46
+ *
47
+ * Only the constructor body itself counts, including nested scopes like FOR and LET. A method
48
+ * called from the constructor is a scope of its own, and is not considered part of the constructor.
49
+ *
50
+ * This is cheap compared to resolving a method reference, so use it to bail out early. */
51
+ enclosingConstructorClass(token: abaplint.Token): string | undefined;
52
+ /** Methods called in an instance constructor are bound statically in ABAP, ie. a redefinition
53
+ * in a sub class is not called. Returns the "{class}.prototype." prefix to call the method with,
54
+ * or undefined if the normal dynamic dispatch via "this." is to be used.
55
+ *
56
+ * Note that the prefix is the class implementing the constructor, which is not necessarily the class
57
+ * being constructed, nor the class defining the method. Eg. constructing a sub class runs the constructor
58
+ * of the super class, and calls in there are bound to the super class. The JS prototype chain then finds
59
+ * the implementation as seen from that class, and interface methods as they are implemented in the class.
60
+ *
61
+ * Only calls written directly in the constructor body are bound statically, see enclosingConstructorClass
62
+ *
63
+ * @param enclosingClass the result of enclosingConstructorClass(token), pass it if already resolved */
64
+ constructorPrototypePrefix(token: abaplint.Token, def: abaplint.Types.MethodDefinition | undefined, enclosingClass?: string): string | undefined;
42
65
  private isBuiltinVariable;
43
66
  isTypePool(token: abaplint.Token): string | undefined;
44
67
  isInterfaceAttribute(token: abaplint.Token): string | undefined;
@@ -60,6 +60,12 @@ class Traversal {
60
60
  static escapeNamespace(name) {
61
61
  return name?.replace(/\//g, "$");
62
62
  }
63
+ /** the self reference "me", it is a FieldChain in eg. MethodCallChain and a SourceField in eg. MethodSource */
64
+ static isMe(node) {
65
+ return (node.get() instanceof abaplint.Expressions.FieldChain
66
+ || node.get() instanceof abaplint.Expressions.SourceField)
67
+ && node.concatTokens().toLowerCase() === "me";
68
+ }
63
69
  static prefixVariable(name) {
64
70
  if (name && keywords_1.DEFAULT_KEYWORDS.has(name)) {
65
71
  return "$" + name;
@@ -391,6 +397,57 @@ class Traversal {
391
397
  }
392
398
  return candidate;
393
399
  }
400
+ /** Returns the name of the class implementing the instance constructor the token is located in,
401
+ * or undefined if the token is not inside an instance constructor.
402
+ *
403
+ * Only the constructor body itself counts, including nested scopes like FOR and LET. A method
404
+ * called from the constructor is a scope of its own, and is not considered part of the constructor.
405
+ *
406
+ * This is cheap compared to resolving a method reference, so use it to bail out early. */
407
+ enclosingConstructorClass(token) {
408
+ let scope = this.findCurrentScopeByToken(token);
409
+ // the token might be in a scope nested inside the method, eg. FOR or LET
410
+ while (scope !== undefined && scope.getIdentifier().stype !== abaplint.ScopeType.Method) {
411
+ if (scope.getIdentifier().stype === abaplint.ScopeType.ClassImplementation) {
412
+ return undefined;
413
+ }
414
+ scope = scope.getParent();
415
+ }
416
+ if (scope === undefined || scope.getIdentifier().sname.toUpperCase() !== "CONSTRUCTOR") {
417
+ return undefined;
418
+ }
419
+ while (scope !== undefined && scope.getIdentifier().stype !== abaplint.ScopeType.ClassImplementation) {
420
+ scope = scope.getParent();
421
+ }
422
+ return scope?.getIdentifier().sname;
423
+ }
424
+ /** Methods called in an instance constructor are bound statically in ABAP, ie. a redefinition
425
+ * in a sub class is not called. Returns the "{class}.prototype." prefix to call the method with,
426
+ * or undefined if the normal dynamic dispatch via "this." is to be used.
427
+ *
428
+ * Note that the prefix is the class implementing the constructor, which is not necessarily the class
429
+ * being constructed, nor the class defining the method. Eg. constructing a sub class runs the constructor
430
+ * of the super class, and calls in there are bound to the super class. The JS prototype chain then finds
431
+ * the implementation as seen from that class, and interface methods as they are implemented in the class.
432
+ *
433
+ * Only calls written directly in the constructor body are bound statically, see enclosingConstructorClass
434
+ *
435
+ * @param enclosingClass the result of enclosingConstructorClass(token), pass it if already resolved */
436
+ constructorPrototypePrefix(token, def, enclosingClass) {
437
+ if (def === undefined
438
+ || def.isStatic() === true
439
+ || def.getVisibility() === abaplint.Visibility.Private
440
+ || def.isAbstract() === true) {
441
+ // static methods are not dispatched via the instance, and private methods cannot be redefined,
442
+ // abstract methods have no implementation to bind to, so keep the dynamic dispatch
443
+ return undefined;
444
+ }
445
+ const className = enclosingClass ?? this.enclosingConstructorClass(token);
446
+ if (className === undefined) {
447
+ return undefined;
448
+ }
449
+ return this.lookupClassOrInterface(className, token) + ".prototype.";
450
+ }
394
451
  isBuiltinVariable(token) {
395
452
  const scope = this.findCurrentScopeByToken(token);
396
453
  if (scope === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abaplint/transpiler",
3
- "version": "2.13.56",
3
+ "version": "2.13.57",
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.20",
32
+ "@abaplint/core": "^2.120.22",
33
33
  "source-map": "^0.7.6"
34
34
  },
35
35
  "devDependencies": {