Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 117 additions & 16 deletions packages/builder/lib/lbt/analyzer/JSModuleAnalyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,78 @@ function getDocumentation(node) {
return undefined;
}

/**
* Builds a map from factory parameter name to "jquery" for every parameter position in the
* dependency array of a sap.ui.define / define call that resolves to the jquery.sap.global
* module.
*
* This supports the common pattern from issue #512 where the jQuery global is received
* under a local parameter name and then used as an alias for jQuery.sap calls:
*
* sap.ui.define(["jquery.sap.global"], function(jq) {
* jq.sap.require("my.module");
* });
*
* The map lifetime is deliberately scoped to one factory body: it is built right before
* visiting that body and is not accessible outside. A parameter in an inner function with the
* same name correctly shadows the outer alias because the outer map is not consulted once the
* inner function's own scope takes over.
*
* @param {object} defineCallNode - The CallExpression node of the define/sap.ui.define call
* @returns {Set<string>} Set of parameter names that are aliases for the jQuery global
*/
function buildJQueryAliasSet(defineCallNode) {
const aliasSet = new Set();
const args = defineCallNode.arguments;
if ( !args || args.length === 0 ) {
return aliasSet;
}

let i = 0;
// skip optional module name string
if ( i < args.length && args[i].type === Syntax.Literal ) {
i++;
}

// find dependency array
if ( i >= args.length || args[i].type !== Syntax.ArrayExpression ) {
return aliasSet;
}
const depArray = args[i].elements;
i++;

// find factory function
if ( i >= args.length || !isCallableExpression(args[i]) ) {
return aliasSet;
}
const params = args[i].params;

// Compare each parameter's dependency with MODULE__JQUERY_SAP_GLOBAL using the canonical
// fromUI5LegacyName helper — the same helper used elsewhere in the analyzer.
for (let p = 0; p < params.length && p < depArray.length; p++) {
const param = params[p];
// Only handle simple identifier parameters (destructuring cannot be a simple alias)
if ( param.type !== Syntax.Identifier ) {
continue;
}
const depValue = getStringValue(depArray[p]);
if ( depValue === undefined ) {
continue;
}
if ( fromUI5LegacyName(depValue) === MODULE__JQUERY_SAP_GLOBAL ) {
aliasSet.add(param.name);
log.verbose(`Local alias '${param.name}' maps to jquery.sap.global`);
}
}
return aliasSet;
}

/**
* Analyzes an already parsed JSDocument to collect information about the contained module(s).
*
* Can handle jQuery.sap.require/jQuery.sap.declare/sap.ui.define and jquery.sap.isDeclared calls.
* Also detects dependencies called via a factory function parameter that is an alias for the
* jquery.sap.global module (see issue #512).
*
* @author Frank Weigel
* @since 1.1.2
Expand Down Expand Up @@ -292,7 +360,7 @@ class JSModuleAnalyzer {
let firstLineBundleName;

// first analyze the whole AST...
visit(ast, false);
visit(ast, false, new Set());

// ...then all the comments
if ( Array.isArray(ast.comments) ) {
Expand Down Expand Up @@ -372,15 +440,26 @@ class JSModuleAnalyzer {
}
}

function visit(node, conditional) {
/**
* Visits an AST node.
*
* @param {object} node - The AST node to visit
* @param {boolean} conditional - Whether the node is reached only conditionally
* @param {Set<string>} jQueryAliases - Set of local parameter names that are aliases
* for the jQuery global (jquery.sap.global) within the current factory body.
* This set is scoped to the factory body of the enclosing sap.ui.define / define call
* and is passed down through the recursion. It is replaced with a fresh set when a
* new factory body is entered, which naturally handles shadowing.
*/
function visit(node, conditional, jQueryAliases) {
// console.log("visiting ", node);

if ( node == null ) {
return;
}

if ( Array.isArray(node) ) {
node.forEach((child) => visit(child, conditional));
node.forEach((child) => visit(child, conditional, jQueryAliases));
return;
}

Expand Down Expand Up @@ -415,8 +494,12 @@ class JSModuleAnalyzer {
iArg++;
}
if ( iArg < args.length && isCallableExpression(args[iArg]) ) {
// unconditionally execute the factory function
visit(args[iArg].body, conditional);
// Build a fresh alias set scoped to this factory body.
// The aliases are derived solely from the factory parameters of this
// define call, so they are naturally scoped to its body.
const factoryAliases = buildJQueryAliasSet(node);
// unconditionally execute the factory function with its own alias set
visit(args[iArg].body, conditional, factoryAliases);
}
} else if ( isMethodCall(node, CALL_REQUIRE_PREDEFINE) || isMethodCall(node, CALL_SAP_UI_PREDEFINE) ) {
// recognized a call to require.predefine() or sap.ui.predefine()
Expand All @@ -436,7 +519,7 @@ class JSModuleAnalyzer {
}
if ( iArg < args.length && isCallableExpression(args[iArg]) ) {
// unconditionally execute the factory function
visit(args[iArg].body, conditional);
visit(args[iArg].body, conditional, jQueryAliases);
}
} else if ( isMethodCall(node, CALL_SAP_UI_REQUIRE) || isMethodCall(node, CALL_AMD_REQUIRE) ) {
// recognized a call to require() or sap.ui.require()
Expand All @@ -455,7 +538,7 @@ class JSModuleAnalyzer {
}
if ( iArg < args.length && isCallableExpression(args[iArg]) ) {
// analyze the callback function
visit(args[iArg].body, conditional);
visit(args[iArg].body, conditional, jQueryAliases);
}
} else if ( isMethodCall(node, CALL_REQUIRE_SYNC) || isMethodCall(node, CALL_SAP_UI_REQUIRE_SYNC) ) {
// recognizes a call to sap.ui.requireSync
Expand All @@ -480,15 +563,33 @@ class JSModuleAnalyzer {
}
info.setFormat(ModuleFormat.UI5_DEFINE);
onRegisterPreloadedModules(node, /* evoSyntax= */ true);
} else if (
// Detect jQuery.sap.require called through a factory parameter alias.
// e.g. sap.ui.define(["jquery.sap.global"], function(jq) { jq.sap.require("..."); })
// The alias set is scoped to the enclosing define factory body.
jQueryAliases.size > 0 &&
node.callee.type === Syntax.MemberExpression &&
node.callee.object.type === Syntax.MemberExpression &&
node.callee.object.object.type === Syntax.Identifier &&
jQueryAliases.has(node.callee.object.object.name) &&
node.callee.object.property.type === Syntax.Identifier &&
node.callee.object.property.name === "sap" &&
node.callee.property.type === Syntax.Identifier &&
node.callee.property.name === "require"
) {
// Treat alias.sap.require(...) exactly like jQuery.sap.require(...)
log.verbose(`Detected jQuery.sap.require via alias '${node.callee.object.object.name}'`);
info.setFormat(ModuleFormat.UI5_LEGACY);
onJQuerySapRequire(node, conditional);
} else if ( isCallableExpression(node.callee) ) {
// recognizes a scope function declaration + argument
visit(node.arguments, conditional);
visit(node.arguments, conditional, jQueryAliases);
// NODE-TODO defaults of callee?
visit(node.callee.body, conditional);
visit(node.callee.body, conditional, jQueryAliases);
} else {
// default visit
for ( const key of condKeys ) {
visit(node[key.key], key.conditional || conditional);
visit(node[key.key], key.conditional || conditional, jQueryAliases);
}
}
break;
Expand All @@ -503,12 +604,12 @@ class JSModuleAnalyzer {
if ( node.test.type == Syntax.UnaryExpression &&
node.test.operator === "!" &&
isMethodCall(node.test.argument, CALL_JQUERY_SAP_IS_DECLARED ) ) {
visit(node.consequent, conditional);
visit(node.alternate, true);
visit(node.consequent, conditional, jQueryAliases);
visit(node.alternate, true, jQueryAliases);
} else {
// default visit
for ( const key of condKeys ) {
visit(node[key.key], key.conditional || conditional);
visit(node[key.key], key.conditional || conditional, jQueryAliases);
}
}
break;
Expand All @@ -517,8 +618,8 @@ class JSModuleAnalyzer {

// 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);
visit(node.key, conditional, jQueryAliases);
visit(node.value, !node.static || conditional, jQueryAliases);

break;

Expand All @@ -529,7 +630,7 @@ class JSModuleAnalyzer {
}
// default visit
for ( const key of condKeys ) {
visit(node[key.key], key.conditional || conditional);
visit(node[key.key], key.conditional || conditional, jQueryAliases);
}
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Feature: issue #512 - local name alias for jQuery.sap.require
// When jquery.sap.global is passed as a factory parameter under a local alias name,
// calls on that alias like alias.sap.require(...) should be detected as dependencies.
sap.ui.define(["jquery.sap.global"], function(jq) {
jq.sap.require("my.module");
jq.sap.require("other.module");
});
Loading