diff --git a/lib/lbt/analyzer/JSModuleAnalyzer.js b/lib/lbt/analyzer/JSModuleAnalyzer.js index dfb1db733..3db428aa4 100644 --- a/lib/lbt/analyzer/JSModuleAnalyzer.js +++ b/lib/lbt/analyzer/JSModuleAnalyzer.js @@ -135,10 +135,10 @@ const EnrichedVisitorKeys = (function() { * All properties in an object pattern are executed. */ ObjectPattern: [], // properties - // PrivateIdentifier: [], // will come with ES2022 + PrivateIdentifier: [], Program: [], Property: [], - // PropertyDefinition: [], // will come with ES2022 + PropertyDefinition: [], /* * argument of the rest element is always executed under the same condition as the rest element itself */ @@ -146,7 +146,7 @@ const EnrichedVisitorKeys = (function() { ReturnStatement: [], SequenceExpression: [], SpreadElement: [], // the argument of the spread operator always needs to be evaluated - argument - // StaticBlock: [], // will come with ES2022 + StaticBlock: [], Super: [], SwitchStatement: [], SwitchCase: ["test", "consequent"], // test and consequent are executed only conditionally @@ -196,15 +196,6 @@ const EnrichedVisitorKeys = (function() { return; } - // Ignore new ES2022 syntax as we currently use ES2021 (see parseUtils.js) - if ( - type === "PrivateIdentifier" || - type === "PropertyDefinition" || - type === "StaticBlock" - ) { - return; - } - const visitorKeys = VisitorKeys[type]; const condKeys = TempKeys[type]; if ( condKeys === undefined ) { @@ -535,6 +526,15 @@ class JSModuleAnalyzer { } break; + case Syntax.PropertyDefinition: + + // Instance properties (static=false) are only initialized when an instance is created (conditional) + // but a computed key is always evaluated on class initialization (eager) + visit(node.key, conditional); + visit(node.value, !node.static || conditional); + + break; + default: if ( condKeys == null ) { log.error("Unhandled AST node type " + node.type, node); diff --git a/lib/lbt/utils/ASTUtils.js b/lib/lbt/utils/ASTUtils.js index 46e4d4f2c..76e27e27c 100644 --- a/lib/lbt/utils/ASTUtils.js +++ b/lib/lbt/utils/ASTUtils.js @@ -56,6 +56,8 @@ export function isMethodCall(node, methodPath) { } export function isNamedObject(node, objectPath, length) { + // TODO: Support PrivateIdentifier (foo.#bar) + // console.log("checking for named object ", node, objectPath, length); while ( length > 1 && node.type === Syntax.MemberExpression && diff --git a/lib/lbt/utils/parseUtils.js b/lib/lbt/utils/parseUtils.js index cd90437d2..a51c66e03 100644 --- a/lib/lbt/utils/parseUtils.js +++ b/lib/lbt/utils/parseUtils.js @@ -7,7 +7,7 @@ export function parseJS(code, userOptions = {}) { // allowed options and their defaults const options = { comment: false, - ecmaVersion: 2021, // NOTE: Adopt JSModuleAnalyzer.js to allow new Syntax when upgrading to newer ECMA versions + ecmaVersion: 2022, // NOTE: Adopt JSModuleAnalyzer.js to allow new Syntax when upgrading to newer ECMA versions range: false, sourceType: "script", }; diff --git a/test/lib/lbt/analyzer/JSModuleAnalyzer.js b/test/lib/lbt/analyzer/JSModuleAnalyzer.js index b584af326..0625dcf2f 100644 --- a/test/lib/lbt/analyzer/JSModuleAnalyzer.js +++ b/test/lib/lbt/analyzer/JSModuleAnalyzer.js @@ -715,6 +715,70 @@ test("LogicalExpression", (t) => { `ui5 module`); }); +test("ES2022: PrivateIdentifier, PropertyDefinition, StaticBlock", (t) => { + const content = ` + sap.ui.define(['require', 'static/module1'], (require) => { + + class TestES2022 { + + // Eager dependencies + + static { + const staticModule2 = sap.ui.requireSync('static/module2'); + } + + static publicStaticField = sap.ui.requireSync('static/module3'); + static #privateStaticField = sap.ui.requireSync('static/module4'); + static [sap.ui.requireSync('static/module5')] = "module5"; + + // Even though the field is on instance level, the computed key is evaluated when the class is declared + [sap.ui.requireSync('static/module6')] = "module6"; + + // Conditional dependencies + + publicField = sap.ui.requireSync('conditional/module1'); + #privateField = sap.ui.requireSync('conditional/module2'); + + #privateMethod() { + sap.ui.requireSync('conditional/module3') + } + + static #privateStaticMethod() { + sap.ui.requireSync('conditional/module4') + } + + } + + });`; + const info = analyzeString(content, "modules/ES2022.js"); + + const expected = [ + "conditional/module1.js", + "conditional/module2.js", + "conditional/module3.js", + "conditional/module4.js", + "static/module1.js", + "static/module2.js", + "static/module3.js", + "static/module4.js", + "static/module5.js", + "static/module6.js", + "ui5loader-autoconfig.js", + ]; + const actual = info.dependencies.sort(); + t.deepEqual(actual, expected, "module dependencies should match"); + expected.forEach((dep) => { + t.is(info.isConditionalDependency(dep), /^conditional\//.test(dep), + `only dependencies to 'conditional/*' modules should be conditional (${dep})`); + t.is(info.isImplicitDependency(dep), !/^(?:conditional|static)\//.test(dep), + `all dependencies other than 'conditional/*' and 'static/*' should be implicit (${dep})`); + }); + t.false(info.dynamicDependencies, + `no use of dynamic dependencies should have been detected`); + t.false(info.rawModule, + `ui5 module`); +}); + test("Dynamic import (declare/require)", async (t) => { const info = await analyze("modules/declare_dynamic_require.js"); t.true(info.dynamicDependencies, diff --git a/test/lib/lbt/utils/parseUtils.js b/test/lib/lbt/utils/parseUtils.js index 92963cbba..9dd5b939d 100644 --- a/test/lib/lbt/utils/parseUtils.js +++ b/test/lib/lbt/utils/parseUtils.js @@ -29,3 +29,9 @@ test("successful parse step (ES2021 features)", (t) => { t.true(ast != null && typeof ast === "object"); t.is(ast.type, "Program"); }); + +test("successful parse step (ES2022 features)", (t) => { + const ast = parseJS("class X { #foo; }"); // Private class field + t.true(ast != null && typeof ast === "object"); + t.is(ast.type, "Program"); +});