diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index d39ad51f0..f8bb5ec32 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -67,6 +67,7 @@ require("./tests/byte-buffer-test"); require("./tests/shared-array-buffer-test"); require("./tests/dex-interface-implementation"); require("./tests/testClassForNameDiscovery"); +require("./tests/testPrebuiltExtendClasses"); require("./tests/testInterfaceImplementation"); require("./tests/testPrimitiveCallbackReturns"); require("./tests/testRuntimeImplementedAPIs"); diff --git a/test-app/app/src/main/assets/app/tests/prebuiltExtend$Dollar.js b/test-app/app/src/main/assets/app/tests/prebuiltExtend$Dollar.js new file mode 100644 index 000000000..4bcc9a145 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/prebuiltExtend$Dollar.js @@ -0,0 +1,5 @@ +exports.DollarFileObject = java.lang.Object.extend({ + toString: function () { + return "dollar file"; + } +}); diff --git a/test-app/app/src/main/assets/app/tests/prebuiltExtendLineOne.mjs b/test-app/app/src/main/assets/app/tests/prebuiltExtendLineOne.mjs new file mode 100644 index 000000000..fa75ec6fd --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/prebuiltExtendLineOne.mjs @@ -0,0 +1 @@ +export const LineOneObject = java.lang.Object.extend({ toString: function () { return "line one"; } }); diff --git a/test-app/app/src/main/assets/app/tests/testPrebuiltExtendClasses.js b/test-app/app/src/main/assets/app/tests/testPrebuiltExtendClasses.js new file mode 100644 index 000000000..9c113e43a --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testPrebuiltExtendClasses.js @@ -0,0 +1,107 @@ +describe("Prebuilt classes of anonymous extends", function () { + + // DexFactory writes the classes it generates at runtime to one of these directories + // (see RuntimeHelper). A class the static binding generator built must be loaded from + // the APK and leave nothing here. + var filesDir = __dirname.replace(/\/app\/tests$/, ""); + var dataDir = filesDir.replace(/\/files$/, ""); + var dexDirs = [dataDir + "/code_cache/secondary-dexes", filesDir + "/secondary-dexes"]; + + function generatedDexFiles(instance) { + var proxyName = instance.getClass().getName().replace(/^com\.tns\.gen\./, ""); + var result = []; + dexDirs.forEach(function (dir) { + var files = new java.io.File(dir).list(); + if (!files) { + return; + } + for (var i = 0; i < files.length; i++) { + var file = files[i]; + if (file.indexOf(proxyName + "-") === 0 && /\.dex$/.test(file)) { + result.push(file); + } + } + }); + return result; + } + + it("loads an anonymous extend's class from the APK", function () { + var Anonymous = java.lang.Object.extend({ + toString: function () { + return "anonymous"; + } + }); + var instance = new Anonymous(); + + expect(instance.toString()).toBe("anonymous"); + expect(generatedDexFiles(instance)).toEqual([]); + }); + + it("loads a TypeScript-compiled native class from the APK", function () { + var CompiledClass = (function (_super) { + __extends(CompiledClass, _super); + function CompiledClass() { + var _this = _super.call(this) || this; + return global.__native(_this); + } + CompiledClass.prototype.toString = function () { + return "compiled"; + }; + return CompiledClass; + }(java.lang.Object)); + var instance = new CompiledClass(); + + expect(instance.toString()).toBe("compiled"); + expect(generatedDexFiles(instance)).toEqual([]); + }); + + it("loads a TypeScript-compiled native class without a constructor from the APK", function () { + var NoConstructorClass = (function (_super) { + __extends(NoConstructorClass, _super); + function NoConstructorClass() { + return _super !== null && _super.apply(this, arguments) || this; + } + NoConstructorClass.prototype.toString = function () { + return "no constructor"; + }; + return NoConstructorClass; + }(java.lang.Object)); + var instance = new NoConstructorClass(); + + expect(instance.toString()).toBe("no constructor"); + expect(generatedDexFiles(instance)).toEqual([]); + }); + + it("loads the class of an extend in a file with $ in its name from the APK", function () { + var DollarFileObject = require("./prebuiltExtend$Dollar").DollarFileObject; + var instance = new DollarFileObject(); + + expect(instance.toString()).toBe("dollar file"); + expect(generatedDexFiles(instance)).toEqual([]); + }); + + it("loads the class of an extend on the first line of an ES module from the APK", function () { + var LineOneObject = require("./prebuiltExtendLineOne.mjs").LineOneObject; + var instance = new LineOneObject(); + + expect(instance.toString()).toBe("line one"); + expect(generatedDexFiles(instance)).toEqual([]); + }); + + it("caches a class generated at runtime under the name DexFactory looks up", function () { + // A computed member hides the extend from the static binding generator. + var ext = "ex" + "tend"; + var RuntimeGenerated = java.lang.Object[ext]({ + toString: function () { + return "runtime generated"; + } + }); + var instance = new RuntimeGenerated(); + + expect(instance.toString()).toBe("runtime generated"); + var files = generatedDexFiles(instance); + expect(files.length).toBe(1); + // ---.dex, as DexFactory.getDexFile probes it. + expect(files[0]).toMatch(/-\d+-\d+-[0-9a-f]{10}\.dex$/); + }); +}); diff --git a/test-app/app/src/main/assets/app/tests/tests.js b/test-app/app/src/main/assets/app/tests/tests.js index 54ede13be..6475937e9 100644 --- a/test-app/app/src/main/assets/app/tests/tests.js +++ b/test-app/app/src/main/assets/app/tests/tests.js @@ -1625,8 +1625,9 @@ describe("Tests ", function () { __log("TEST: TestCanParseSignatureWithTypesThatContainsCapitalLettersForPrimitiveTypes"); var formats = java.lang.reflect.Array.newInstance(java.text.NumberFormat.class, 2); - formats[0] = java.text.NumberFormat.getInstance(); - formats[1] = java.text.NumberFormat.getIntegerInstance(); + // The pattern below uses "," as a separator, which is a decimal separator in many locales. + formats[0] = java.text.NumberFormat.getInstance(java.util.Locale.US); + formats[1] = java.text.NumberFormat.getIntegerInstance(java.util.Locale.US); var mf = new java.text.MessageFormat("{0}, {1}") mf.setFormats(formats); var arr = mf.parse("123, 4567"); diff --git a/test-app/build-tools/jsparser/tests/cases/extends_ts_no_constructor/app/no_constructor.js b/test-app/build-tools/jsparser/tests/cases/extends_ts_no_constructor/app/no_constructor.js new file mode 100644 index 000000000..1e545360d --- /dev/null +++ b/test-app/build-tools/jsparser/tests/cases/extends_ts_no_constructor/app/no_constructor.js @@ -0,0 +1,11 @@ +var NoConstructor = /** @class */ (function (_super) { + __extends(NoConstructor, _super); + function NoConstructor() { + return _super !== null && _super.apply(this, arguments) || this; + } + NoConstructor.prototype.toString = function () { + return "no constructor"; + }; + return NoConstructor; +}(java.lang.Object)); +var Minified=function(e){__extends(Minified,e);function Minified(){return null!==e&&e.apply(this,arguments)||this}Minified.prototype.toString=function(){return"minified"};return Minified}(java.lang.Object); diff --git a/test-app/build-tools/jsparser/tests/cases/extends_ts_no_constructor/internal/ts_helpers.js b/test-app/build-tools/jsparser/tests/cases/extends_ts_no_constructor/internal/ts_helpers.js new file mode 100644 index 000000000..1a0625f45 --- /dev/null +++ b/test-app/build-tools/jsparser/tests/cases/extends_ts_no_constructor/internal/ts_helpers.js @@ -0,0 +1,175 @@ +(function () { + + var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length; + var r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + + if (typeof global.Reflect === "object" && typeof global.Reflect.decorate === "function") { + r = global.Reflect.decorate(decorators, target, key, desc); + } + else { + for (var i = decorators.length - 1; i >= 0; i--) { + if (d = decorators[i]) { + r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + } + } + } + return c > 3 && r && Object.defineProperty(target, key, r), r; + }; + + // For backward compatibility. + var __native = function (thiz) { + // we are setting the __container__ property to the base class when the super method is called + // if the constructor returns the __native(this) call we will use the old implementation + // copying all the properties to the result + // otherwise if we are using the result from the super() method call we won't need such logic + // as thiz already contains the parent properties + // this way we now support both implementations in typescript generated constructors: + // 1: super(); return __native(this); + // 2: return super() || this; + if (thiz.__container__) { + var result = thiz.__proto__; + + for (var prop in thiz) { + if (thiz.hasOwnProperty(prop)) { + thiz.__proto__[prop] = thiz[prop]; + delete thiz[prop]; + } + } + + thiz.constructor = undefined; + thiz.__proto__ = undefined; + Object.freeze(thiz); + Object.preventExtensions(thiz); + return result; + } else { + return thiz; + } + }; + + var __extends = function (Child, Parent) { + var extendNativeClass = !!Parent.extend && (Parent.extend.toString().indexOf("[native code]") > -1); + if (!extendNativeClass) { + __extends_ts(Child, Parent); + return; + } + if (Parent.__isPrototypeImplementationObject) { + throw new Error("Can not extend an already extended native object."); + } + + function extend(thiz) { + var child = thiz.__proto__.__child; + if (!child.__extended) { + var parent = thiz.__proto__.__parent; + child.__extended = parent.extend(child.name, child.prototype, true); + // This will deal with "i instanceof child" + child[Symbol.hasInstance] = function (instance) { + return instance instanceof this.__extended; + } + } + return child.__extended; + }; + + Parent.__activityExtend = function (parent, name, implementationObject) { + __log("__activityExtend called"); + return parent.extend(name, implementationObject); + }; + + Parent.call = function (thiz) { + var Extended = extend(thiz); + thiz.__container__ = true; + if (arguments.length > 1) { + thiz.__proto__ = new (Function.prototype.bind.apply(Extended, [null].concat(Array.prototype.slice.call(arguments, 1)))); + } + else { + thiz.__proto__ = new Extended() + } + return thiz.__proto__; + }; + + Parent.apply = function (thiz, args) { + var Extended = extend(thiz); + thiz.__container__ = true; + if (args && args.length > 0) { + thiz.__proto__ = new (Function.prototype.bind.apply(Extended, [null].concat(args))); + } + else { + thiz.__proto__ = new Extended(); + } + return thiz.__proto__; + }; + __extends_ns(Child, Parent); + Child.__isPrototypeImplementationObject = true; + Child.__proto__ = Parent; + Child.prototype.__parent = Parent; + Child.prototype.__child = Child; + } + + var __extends_ts = function (child, parent) { + extendStaticFunctions(child, parent); + assignPrototypeFromParentToChild(parent, child); + }; + + var __extends_ns = function (child, parent) { + if (!parent.extend) { + assignPropertiesFromParentToChild(parent, child); + } + + assignPrototypeFromParentToChild(parent, child); + }; + + var extendStaticFunctions = + Object.setPrototypeOf + || (hasInternalProtoProperty() && function (child, parent) { child.__proto__ = parent; }) + || assignPropertiesFromParentToChild; + + function hasInternalProtoProperty() { + return { __proto__: [] } instanceof Array; + } + + function assignPropertiesFromParentToChild(parent, child) { + for (var property in parent) { + if (parent.hasOwnProperty(property)) { + child[property] = parent[property]; + } + } + } + + function assignPrototypeFromParentToChild(parent, child) { + function __() { + this.constructor = child; + } + + if (parent === null) { + child.prototype = Object.create(null); + } else { + __.prototype = parent.prototype; + child.prototype = new __(); + } + } + + + function JavaProxy(className) { + return function (target) { + var extended = target.extend(className, target.prototype) + extended.name = className; + return extended; + }; + } + + function Interfaces(interfacesArr) { + return function (target) { + if (interfacesArr instanceof Array) { + // attach interfaces: [] to the object + target.prototype.interfaces = interfacesArr; + } + } + } + + Object.defineProperty(global, "__native", { value: __native }); + Object.defineProperty(global, "__extends", { value: __extends }); + Object.defineProperty(global, "__decorate", { value: __decorate }); + + global.JavaProxy = JavaProxy; + global.Interfaces = Interfaces; +})() \ No newline at end of file diff --git a/test-app/build-tools/jsparser/tests/specs/ast-parser-tests.spec.js b/test-app/build-tools/jsparser/tests/specs/ast-parser-tests.spec.js index a05c6b4fe..a72be6651 100644 --- a/test-app/build-tools/jsparser/tests/specs/ast-parser-tests.spec.js +++ b/test-app/build-tools/jsparser/tests/specs/ast-parser-tests.spec.js @@ -243,6 +243,34 @@ describe("parser/js_parser tests", function () { }); }); + it("Generated metadata for a transpiled typescript class without a constructor should contain the super call location", function (done) { + let input = path.normalize(path.join(prefix, "extends_ts_no_constructor", "app")), + generatedJavaClassesRoot = path.normalize(path.join(prefix, "extends_ts_no_constructor", "src", "main", "java")); + + clearOutput(); + + execGradle(input, generatedJavaClassesRoot, function (error, stdout, stderr) { + if (error) { + console.error(`exec error: ${error}`); + return done.fail(error); + } + + logExecResult(stdout, stderr) + + let bindingsContent = fs.readFileSync(sbgBindingOutoutFile, "utf-8").toString().trim().split('\n'); + + // JavaClass*file*line*column*NewClass*...: the runtime names the class from + // the location of the `_super.apply` call (the column of `apply`, 1-based). + let locations = bindingsContent.map(line => line.split("*").slice(1, 5).join("*")).sort(); + expect(locations).toEqual([ + "no_constructor*11*87*Minified", + "no_constructor*4*42*NoConstructor" + ]); + + done(); + }); + }); + it("Generated metadata for bindings should return proper JavaClass name when there is a static property and double assignment", function (done) { let input = path.normalize(path.join(prefix, "decorated_double_assignment_ts", "app")), generatedJavaClassesRoot = path.normalize(path.join(prefix, "decorated_double_assignment_ts", "src", "main", "java")); diff --git a/test-app/build-tools/jsparser/visitors/es5-visitors.js b/test-app/build-tools/jsparser/visitors/es5-visitors.js index c857bff40..ca6a8c0ba 100644 --- a/test-app/build-tools/jsparser/visitors/es5-visitors.js +++ b/test-app/build-tools/jsparser/visitors/es5-visitors.js @@ -257,23 +257,54 @@ var es5_visitors = (function() { return matchingNodes.length > 0 ? matchingNodes[0].declarations[0].init.left : null; } + + /** Getting the super.apply node from the return of a class without a constructor + return _super !== null && _super.apply(this, arguments) || this; + return _super.apply(this, arguments) || this; + */ + function getReturnSuperCallLineNode(nodes, superIdentifier) { + function isSuperCall(node) { + return types.isCallExpression(node) && + types.isMemberExpression(node.callee) && + node.callee.object.name === superIdentifier; + } + + for (var i = 0; i < nodes.length; i++) { + var argument = nodes[i].argument; + if (!types.isLogicalExpression(argument)) { + continue; + } + var left = argument.left; + if (isSuperCall(left)) { + return left; + } + if (types.isLogicalExpression(left) && isSuperCall(left.right)) { + return left.right; + } + } + return null; + } //// if (superVariableIdentifier) { var possibleVariableDeclarations = []; var possibleExpressionStatements = []; + var possibleReturnStatements = []; constructorFunctionScope.body.body.forEach(node => { if (types.isVariableDeclaration(node)) { possibleVariableDeclarations.push(node); } else if (types.isExpressionStatement(node)) { possibleExpressionStatements.push(node); + } else if (types.isReturnStatement(node)) { + possibleReturnStatements.push(node); } }); - if (possibleVariableDeclarations.length > 0 || possibleExpressionStatements.length > 0) { + if (possibleVariableDeclarations.length > 0 || possibleExpressionStatements.length > 0 || possibleReturnStatements.length > 0) { var superCallRHS = getThisDeclarationSuperCallLineNode(possibleVariableDeclarations, superVariableIdentifier) || - getThisAssignmentSuperCallLineNode(possibleExpressionStatements, superVariableIdentifier); + getThisAssignmentSuperCallLineNode(possibleExpressionStatements, superVariableIdentifier) || + getReturnSuperCallLineNode(possibleReturnStatements, superVariableIdentifier); if (superCallRHS) { var superCallee = superCallRHS.callee.property; diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index 1e7fc2325..fb87868c4 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -188,6 +188,7 @@ add_library( src/main/cpp/ErrorEvents.cpp src/main/cpp/EventLoop.cpp src/main/cpp/Events.cpp + src/main/cpp/ExtendFileIdentifier.cpp src/main/cpp/FrameCallbacks.cpp src/main/cpp/FieldAccessor.cpp src/main/cpp/File.cpp diff --git a/test-app/runtime/src/main/cpp/ExtendFileIdentifier.cpp b/test-app/runtime/src/main/cpp/ExtendFileIdentifier.cpp new file mode 100644 index 000000000..3ebbea883 --- /dev/null +++ b/test-app/runtime/src/main/cpp/ExtendFileIdentifier.cpp @@ -0,0 +1,157 @@ +#include "ExtendFileIdentifier.h" + +#include +#include + +namespace { + +void Md5(const std::string& input, uint8_t digest[16]) { + static const uint32_t K[64] = { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + }; + static const int S[64] = { + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + }; + + std::vector message(input.begin(), input.end()); + uint64_t bitLength = static_cast(input.size()) * 8; + message.push_back(0x80); + while (message.size() % 64 != 56) { + message.push_back(0); + } + for (int i = 0; i < 8; i++) { + message.push_back(static_cast(bitLength >> (8 * i))); + } + + uint32_t state[4] = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476}; + for (size_t offset = 0; offset < message.size(); offset += 64) { + uint32_t words[16]; + for (int i = 0; i < 16; i++) { + const uint8_t* p = &message[offset + i * 4]; + words[i] = static_cast(p[0]) | (static_cast(p[1]) << 8) | + (static_cast(p[2]) << 16) | (static_cast(p[3]) << 24); + } + + uint32_t a = state[0], b = state[1], c = state[2], d = state[3]; + for (int i = 0; i < 64; i++) { + uint32_t f; + int g; + if (i < 16) { + f = (b & c) | (~b & d); + g = i; + } else if (i < 32) { + f = (d & b) | (~d & c); + g = (5 * i + 1) % 16; + } else if (i < 48) { + f = b ^ c ^ d; + g = (3 * i + 5) % 16; + } else { + f = c ^ (b | ~d); + g = (7 * i) % 16; + } + f = f + a + K[i] + words[g]; + a = d; + d = c; + c = b; + b = b + ((f << S[i]) | (f >> (32 - S[i]))); + } + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + } + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + digest[i * 4 + j] = static_cast(state[i] >> (8 * j)); + } + } +} + +// Java's String.split("_"): keeps leading and inner empty parts, drops trailing ones. +std::vector SplitLikeJava(const std::string& value) { + std::vector parts; + size_t start = 0; + while (true) { + size_t end = value.find('_', start); + if (end == std::string::npos) { + parts.push_back(value.substr(start)); + break; + } + parts.push_back(value.substr(start, end - start)); + start = end + 1; + } + while (!parts.empty() && parts.back().empty()) { + parts.pop_back(); + } + return parts; +} + +bool IsJavaIdentifierPart(unsigned char c) { + // Non-ASCII bytes are kept: the generator keeps letters outside ASCII as they are. + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || + c == '_' || c == '$' || c >= 0x80; +} + +} + +namespace tns { + +std::string ExtendFileIdentifier(const std::string& relativePathWithoutExtension) { + std::string file = relativePathWithoutExtension; + for (char& ch : file) { + if (ch == '-' || ch == '\\' || ch == '/' || ch == '.' || ch == ' ') { + ch = '_'; + } + } + + if (file.empty()) { + return "unknown"; + } + + std::vector parts = SplitLikeJava(file); + std::string identifier; + size_t first = parts.size() > 3 ? parts.size() - 3 : 0; + for (size_t i = first; i < parts.size(); i++) { + if (!identifier.empty()) { + identifier += '_'; + } + identifier += parts[i]; + } + + uint8_t digest[16]; + Md5(file, digest); + static const char hex[] = "0123456789abcdef"; + identifier += '_'; + for (int i = 0; i < 3; i++) { + identifier += hex[digest[i] >> 4]; + identifier += hex[digest[i] & 0xf]; + } + + for (char& ch : identifier) { + if (!IsJavaIdentifierPart(static_cast(ch))) { + ch = '_'; + } + } + return identifier; +} + +} diff --git a/test-app/runtime/src/main/cpp/ExtendFileIdentifier.h b/test-app/runtime/src/main/cpp/ExtendFileIdentifier.h new file mode 100644 index 000000000..07d9c9d74 --- /dev/null +++ b/test-app/runtime/src/main/cpp/ExtendFileIdentifier.h @@ -0,0 +1,21 @@ +#ifndef EXTENDFILEIDENTIFIER_H_ +#define EXTENDFILEIDENTIFIER_H_ + +#include + +namespace tns { + +/** + * The file part of an anonymous extended class's name, for a script path relative to the + * app root with its extension removed. + * + * Must stay identical to the static binding generator (the file name built in + * jsparser's es5-visitors.js, then Generator.generateUniqueFileIdentifier and + * Generator.getNormalizedName). A different name makes the runtime miss the prebuilt + * class and generate one at runtime. + */ +std::string ExtendFileIdentifier(const std::string& relativePathWithoutExtension); + +} + +#endif /* EXTENDFILEIDENTIFIER_H_ */ diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index 5691efcf7..adb1b3ebe 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -17,6 +17,7 @@ #include #include #include "ManualInstrumentation.h" +#include "ExtendFileIdentifier.h" #include "JSONObjectHelper.h" @@ -2078,6 +2079,8 @@ bool MetadataNode::GetExtendLocation(v8::Isolate* isolate, string& extendLocatio string srcFileName = ArgConverter::ConvertToString(scriptName); string fullPathToFile; + // ES modules (`.mjs`, and HTTP-loaded modules) have no CommonJS wrapper. + bool hasCommonJsPrologue = true; if (srcFileName == "") { // Corner case, extend call is coming from the heap snapshot script // This is possible for lazily compiled code - e.g. from the body of a function @@ -2105,6 +2108,7 @@ bool MetadataNode::GetExtendLocation(v8::Isolate* isolate, string& extendLocatio stripPrefix(normalized, "file://"); if (normalized.rfind("http://", 0) == 0 || normalized.rfind("https://", 0) == 0) { + hasCommonJsPrologue = false; size_t schemeEnd = normalized.find("://"); size_t pathStart = normalized.find('/', schemeEnd + 3); if (pathStart == string::npos) { @@ -2129,39 +2133,13 @@ bool MetadataNode::GetExtendLocation(v8::Isolate* isolate, string& extendLocatio s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; }; if (endsWith(normalized, ".mjs")) { + hasCommonJsPrologue = false; normalized.resize(normalized.size() - 4); } else if (endsWith(normalized, ".js")) { normalized.resize(normalized.size() - 3); } - fullPathToFile = normalized; - - for (char& ch : fullPathToFile) { - const unsigned char c = static_cast(ch); - const bool isIdentifierChar = - (c >= 'A' && c <= 'Z') || - (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || - ch == '_'; - if (!isIdentifierChar) { - ch = '_'; - } - } - - std::vector pathParts; - Util::SplitString(fullPathToFile, "_", pathParts); - - std::string lastPathPart; - for (auto it = pathParts.rbegin(); it != pathParts.rend(); ++it) { - if (!it->empty()) { - lastPathPart = *it; - break; - } - } - if (lastPathPart.empty()) { - lastPathPart = "script"; - } - fullPathToFile = lastPathPart; + fullPathToFile = ExtendFileIdentifier(normalized); } int lineNumber = frame->GetLineNumber(); @@ -2180,7 +2158,7 @@ bool MetadataNode::GetExtendLocation(v8::Isolate* isolate, string& extendLocatio // Account for the column length offset added by the addition of the Common JS function wrapper // See issue https://github.com/NativeScript/android-runtime/issues/975 - if (lineNumber == 1) { + if (lineNumber == 1 && hasCommonJsPrologue) { column = column - ModuleInternal::MODULE_PROLOGUE_LENGTH; } diff --git a/test-app/runtime/src/main/java/com/tns/DexFactory.java b/test-app/runtime/src/main/java/com/tns/DexFactory.java index 1bf7ee7ed..cd71f13b3 100644 --- a/test-app/runtime/src/main/java/com/tns/DexFactory.java +++ b/test-app/runtime/src/main/java/com/tns/DexFactory.java @@ -82,25 +82,18 @@ public Class resolveClass(String baseClassName, String name, String className String fullClassName = className.replace("$", "_"); String originalFullClassName = fullClassName; - // try to get pre-generated binding classes - try { - if (logger.isEnabled()) { - logger.write("getting pre-generated proxy class with name: " + fullClassName.replace("-", "_")); - } - - Class pregeneratedClass = classLoader.loadClass(fullClassName.replace("-", "_")); - - if (logger.isEnabled()) { - logger.write("Pre-generated class found: " + fullClassName.replace("-", "_")); - } - + // The static binding generator keeps a `$` from the script's file name in an + // anonymous extend's class name; the runtime already replaced the base class's `$`. + Class pregeneratedClass = null; + if (!fullClassName.equals(className)) { + pregeneratedClass = loadPregeneratedClass(className.replace("-", "_")); + } + if (pregeneratedClass == null) { + pregeneratedClass = loadPregeneratedClass(fullClassName.replace("-", "_")); + } + if (pregeneratedClass != null) { return pregeneratedClass; - } catch (Exception e) { - if (logger.isEnabled()) { - logger.write("Pre-generated class not found: " + fullClassName.replace("-", "_")); - } } - // // new: com.tns.gen.android.widget.DatePicker_MyActivity_59_56_ // old: com.tns.tests.Button1_fMyActivity_l56_c44__MyButton @@ -248,6 +241,27 @@ public static String strJoin(String[] array, String separator) { return sbStr.toString(); } + private Class loadPregeneratedClass(String name) { + try { + if (logger.isEnabled()) { + logger.write("getting pre-generated proxy class with name: " + name); + } + + Class pregeneratedClass = classLoader.loadClass(name); + + if (logger.isEnabled()) { + logger.write("Pre-generated class found: " + name); + } + + return pregeneratedClass; + } catch (Exception e) { + if (logger.isEnabled()) { + logger.write("Pre-generated class not found: " + name); + } + return null; + } + } + private String getClassToProxyName(String className) throws InvalidClassException { String classToProxy = className; @@ -348,7 +362,7 @@ private String generateDex(String proxyName, String contentDigest, String classN } AnnotationDescriptor[] annotations = null; - return proxyGenerator.generateProxy(proxyName, new ClassInfo(classToProxy), methodOverridesSet, implementedInterfacesSet, isInterface, annotations); + return proxyGenerator.generateProxy(proxyName, contentDigest, new ClassInfo(classToProxy), methodOverridesSet, implementedInterfacesSet, isInterface, annotations); } private void updateDexThumbAndPurgeCache() {