@ui5/builder
Version:
UI5 Tooling - Builder
591 lines (541 loc) • 20.7 kB
JavaScript
"use strict";
const esprima = require("esprima");
const {Syntax} = esprima;
const escope = require("escope");
const ModuleName = require("../utils/ModuleName");
const {Format: ModuleFormat} = require("../resources/ModuleInfo");
const UI5ClientConstants = require("../UI5ClientConstants");
const {findOwnProperty, getLocation, getPropertyKey, isMethodCall, isString} = require("../utils/ASTUtils");
const log = require("@ui5/logger").getLogger("lbt:analyzer:JSModuleAnalyzer");
// ------------------------------------------------------------------------------------------------------------------
const EnrichedVisitorKeys = (function() {
const VisitorKeys = require("estraverse").VisitorKeys;
function toBeDone() {
return null;
}
/*
* The following object contains for each known estree node type
* a list of visitor keys that represent conditionally executed code branches.
* E.g. in an IfExpression, the 'test' is always executed, whereas 'consequent'
* and 'alternate' are only executed under certain conditions.
*
* While visiting the AST of a JavaSCript file, the JSModuleAnalyzer uses this information
* to decide whether a code block is executed conditionally or unconditionally.
* Besides this information which is inherent to the language, the analyzer uses
* additional knowledge about special APIS / constructs (e.g. the factory function of
* an AMD module is known to be executed when the module is executed, an IIFE is known to
* be executed etc.)
*
* To be more robust against the evolution of the language, the object below is checked
* against the 'official' list of node types and node keys as defined by 'estraverse'.
* This helps to ensure that no new syntax addition is missed and that the configured
* keys are valid.
*/
const TempKeys = {
AssignmentExpression: [],
/*
* function( >>>a=3<<<, b) {...}
* var [>>>a=3<<<, b] = [...];
*
* The default value expression (right) is only evaluated when there's no other value in
* the context of the pattern (e.g. destructuring or function call don't provide a value),
* so it's a conditional branch.
*/
AssignmentPattern: ["right"],
ArrayExpression: [],
/*
* var >>>[a=3, b]<<< = [...];
* All elements in an array pattern are unconditional.
*/
ArrayPattern: [], // elements
/*
* The body of an arrow function is only executed when the arrow function is executed
*/
ArrowFunctionExpression: ["body"],
/*
* The argument of await is always executed
* TODO how to handle code after the await expression?
*/
AwaitExpression: [], // argument
BlockStatement: [],
BinaryExpression: [],
BreakStatement: [],
CallExpression: [], // special handling
CatchClause: ["param", "body"],
ClassBody: [],
ClassDeclaration: [],
ClassExpression: [],
ComprehensionBlock: toBeDone(["left", "right"]), // CAUTION: It's deferred to ES7.
ComprehensionExpression: toBeDone(), // CAUTION: It's deferred to ES7.
ConditionalExpression: ["consequent", "alternate"],
ContinueStatement: [],
DebuggerStatement: [],
DirectiveStatement: [],
/*
* 'condition' is executed on the same conditions as the surrounding block, potentially repeated,
* 'block' is always entered and might be repeated
*/
DoWhileStatement: [],
EmptyStatement: [],
ExportAllDeclaration: [], // no parts of an export are conditional - source
ExportDefaultDeclaration: [], // no parts of an export are conditional - declaration
ExportNamedDeclaration: [], // no parts of an export are conditional - declaration, specifiers, source
ExportSpecifier: [], // no parts of an export are conditional exported, local
ExpressionStatement: [],
ForStatement: ["update", "body"],
ForInStatement: ["body"],
ForOfStatement: ["body"],
FunctionDeclaration: ["body"], // a nested function is potentially 'conditional'
FunctionExpression: ["body"], // a nested function is potentially 'conditional'
GeneratorExpression: toBeDone(["blocks", "filter", "body"]), // CAUTION: It's deferred to ES7.
Identifier: [],
IfStatement: ["consequent", "alternate"],
/*
* all parts of an import declaration are executed unconditionally
*/
ImportDeclaration: [], // specifiers, source
/*
* import >>>a<<< from 'module';
*/
ImportDefaultSpecifier: [], // local
/*
* import >>>* as b<<< from 'module';
*/
ImportNamespaceSpecifier: [], // local
/*
* import {>>>a as c<<<,b} from 'module';
*/
ImportSpecifier: [], // imported, local
Literal: [],
LabeledStatement: [],
LogicalExpression: [],
MemberExpression: [],
MetaProperty: toBeDone(["meta", "property"]),
MethodDefinition: [],
ModuleSpecifier: [],
NewExpression: [],
ObjectExpression: [],
/*
* >>>{a,b,c}<<< = {...}
*
* All properties in an object pattern are executed.
*/
ObjectPattern: [], // properties
Program: [],
Property: [],
/*
* argument of the rest element is always executed under the same condition as the rest element itself
*/
RestElement: [], // argument
ReturnStatement: [],
SequenceExpression: [],
SpreadElement: [], // the argument of the spread operator always needs to be evaluated - argument
Super: [],
SwitchStatement: [],
SwitchCase: ["test", "consequent"], // test and consequent are executed only conditionally
/*
* all parts of a tagged template literal are executed under the same condition as the context
*/
TaggedTemplateExpression: [], // tag, quasi
TemplateElement: [],
/*
* all parts of a template literal are executed under the same condition as the context
*/
TemplateLiteral: [], // quasis, expressions
ThisExpression: [],
ThrowStatement: [],
TryStatement: ["handler"], // handler is called conditionally
UnaryExpression: [],
UpdateExpression: [],
VariableDeclaration: [],
VariableDeclarator: [],
/*
* 'condition' is executed on the same conditions as the surrounding block and potentially repeated,
* 'block' maybe entered only conditionally but can be repeated
*/
WhileStatement: ["body"],
WithStatement: [],
YieldExpression: []
};
// check for unknown keys in our configuration
for ( const type in TempKeys ) {
if ( VisitorKeys[type] === undefined ) {
throw new Error("configuration contains unknown node type '" + type + "'");
}
}
// merge with 'official' visitor keys
Object.keys(VisitorKeys).forEach( (type) => {
const visitorKeys = VisitorKeys[type];
const condKeys = TempKeys[type];
if ( condKeys === undefined ) {
// configuration missing in ConditionalKeys, maybe a new syntax ?
throw new Error(`unknown estree node type '${type}', new syntax?`);
} else if ( Array.isArray(condKeys) ) {
// check configured keys against visitor keys
condKeys.forEach( (key) => {
if ( visitorKeys.indexOf(key) < 0 ) {
throw new Error(`configuration for type '${type}' contains unknown key '${key}'`);
}
});
TempKeys[type] = visitorKeys.map( (key) => ({
key: key,
conditional: condKeys.indexOf(key) >= 0
}) );
} else {
// this is a 'toBeDone' node type, keep null and complain at runtime when such a node occurs
}
});
return TempKeys;
}());
const CALL_AMD_DEFINE = ["define"];
const CALL_AMD_REQUIRE = ["require"];
const CALL_REQUIRE_SYNC = ["require", "sync"];
const CALL_REQUIRE_PREDEFINE = ["require", "predefine"];
const CALL_REQUIRE_PRELOAD = ["require", "preload"];
const CALL_SAP_UI_DEFINE = ["sap", "ui", "define"];
const CALL_SAP_UI_REQUIRE = ["sap", "ui", "require"];
const CALL_SAP_UI_REQUIRE_SYNC = ["sap", "ui", "requireSync"];
const CALL_SAP_UI_REQUIRE_PRELOAD = ["sap", "ui", "require", "preload"];
const CALL_SAP_UI_PREDEFINE = ["sap", "ui", "predefine"];
const CALL_JQUERY_SAP_DECLARE = [["jQuery", "$"], "sap", "declare"];
const CALL_JQUERY_SAP_IS_DECLARED = [["jQuery", "$"], "sap", "isDeclared"];
const CALL_JQUERY_SAP_REQUIRE = [["jQuery", "$"], "sap", "require"];
const CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES = [["jQuery", "$"], "sap", "registerPreloadedModules"];
function isCallableExpression(node) {
return node.type == Syntax.FunctionExpression || node.type == Syntax.ArrowFunctionExpression;
}
/**
* 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.
*
* @author Frank Weigel
* @since 1.1.2
* @private
*/
class JSModuleAnalyzer {
analyze(ast, defaultName, info) {
let nModuleDeclarations = 0;
// console.log(JSON.stringify(ast, null, " "));
visit(ast, false);
if ( info.format === ModuleFormat.UI5_LEGACY ) {
info.addImplicitDependency(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL);
} else if ( info.format === ModuleFormat.UI5_DEFINE ) {
info.addImplicitDependency(UI5ClientConstants.MODULE__UI5LOADER_AUTOCONFIG);
}
if ( nModuleDeclarations === 0 && info.name == null ) {
info.name = defaultName;
}
if ( info.dependencies.length === 0 && info.subModules.length === 0 ) {
info.rawModule = true;
}
const scopeManager = escope.analyze(ast);
const currentScope = scopeManager.acquire(ast); // global scope
if ( currentScope.set.size > 0 ) {
info.requiresTopLevelScope = true;
info.exposedGlobals = Array.from(currentScope.set.keys());
// console.log(info.name, info.exposedGlobals);
}
return;
// hoisted functions
function visit(node, conditional) {
// console.log("visiting ", node);
if ( node == null ) {
return;
}
if ( Array.isArray(node) ) {
node.forEach((child) => visit(child, conditional));
return;
}
const condKeys = EnrichedVisitorKeys[node.type];
switch (node.type) {
case Syntax.CallExpression:
if ( !conditional && isMethodCall(node, CALL_JQUERY_SAP_DECLARE) ) {
// recognized a call to jQuery.sap.declare()
nModuleDeclarations++;
info.setFormat(ModuleFormat.UI5_LEGACY);
onDeclare(node);
} else if ( !conditional
&& (isMethodCall(node, CALL_SAP_UI_DEFINE) || isMethodCall(node, CALL_AMD_DEFINE)) ) {
// recognized a call to define() or sap.ui.define()
// console.log("**** recognized a call to sap.ui.define");
nModuleDeclarations++;
if ( isMethodCall(node, CALL_SAP_UI_DEFINE) ) {
info.setFormat(ModuleFormat.UI5_DEFINE);
} else {
info.setFormat(ModuleFormat.AMD);
}
onDefine(node);
const args = node.arguments;
let iArg = 0;
if ( iArg < args.length && isString(args[iArg]) ) {
iArg++;
}
if ( iArg < args.length && args[iArg].type == Syntax.ArrayExpression ) {
iArg++;
}
if ( iArg < args.length && isCallableExpression(args[iArg]) ) {
// unconditionally execute the factory function
visit(args[iArg].body, conditional);
}
} else if ( isMethodCall(node, CALL_REQUIRE_PREDEFINE) || isMethodCall(node, CALL_SAP_UI_PREDEFINE) ) {
// recognized a call to require.predefine() or sap.ui.predefine()
info.setFormat(ModuleFormat.UI5_DEFINE);
onSapUiPredefine(node);
} else if ( isMethodCall(node, CALL_SAP_UI_REQUIRE) || isMethodCall(node, CALL_AMD_REQUIRE) ) {
// recognized a call to require() or sap.ui.require()
if ( isMethodCall(node, CALL_SAP_UI_REQUIRE) ) {
info.setFormat(ModuleFormat.UI5_DEFINE);
} else {
info.setFormat(ModuleFormat.AMD);
}
let iArg = 0;
const args = node.arguments;
if ( iArg < args.length && args[iArg].type == Syntax.ArrayExpression ) {
// TODO onAsyncRequire(node, node.getChild(1));
// requireJS signature, handle as such
analyzeDependencyArray(args[iArg].elements, conditional, null);
iArg++;
}
if ( iArg < args.length && isCallableExpression(args[iArg]) ) {
// analyze the callback function
visit(args[iArg].body, conditional);
}
} else if ( isMethodCall(node, CALL_REQUIRE_SYNC) || isMethodCall(node, CALL_SAP_UI_REQUIRE_SYNC) ) {
// recognizes a call to sap.ui.requireSync
info.setFormat(ModuleFormat.UI5_DEFINE);
onSapUiRequireSync(node, conditional);
} else if ( isMethodCall(node, CALL_JQUERY_SAP_REQUIRE) ) {
// recognizes a call to jQuery.sap.require
info.setFormat(ModuleFormat.UI5_LEGACY);
onJQuerySapRequire(node, conditional);
} else if ( isMethodCall(node, CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES)
|| isMethodCall(node, CALL_REQUIRE_PRELOAD)
|| isMethodCall(node, CALL_SAP_UI_REQUIRE_PRELOAD) ) {
// recognizes a call to jQuery.sap.registerPreloadedModules
const legacyCall = isMethodCall(node, CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES);
info.setFormat( legacyCall ? ModuleFormat.UI5_LEGACY : ModuleFormat.UI5_DEFINE);
onRegisterPreloadedModules(node, legacyCall);
} else if ( isCallableExpression(node.callee) ) {
// recognizes a scope function declaration + argument
visit(node.arguments, conditional);
// NODE-TODO defaults of callee?
visit(node.callee.body, conditional);
} else {
// default visit
for ( const key of condKeys ) {
visit(node[key.key], key.conditional || conditional);
}
}
break;
case Syntax.IfStatement:
// recognizes blocks of the form
// if ( !jQuery.sap.isDeclared() ) {
// ...
// }
// required for the analysis of files that have been build with the
// embedding merge writer (e.g. sap-ui-core-all.js)
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);
} else {
// default visit
for ( const key of condKeys ) {
visit(node[key.key], key.conditional || conditional);
}
}
break;
default:
if ( condKeys == null ) {
log.error("Unhandled AST node type " + node.type, node);
throw new Error(`Unhandled AST node type ${node.type}`);
}
// default visit
for ( const key of condKeys ) {
visit(node[key.key], key.conditional || conditional);
}
break;
}
}
function onDeclare(node) {
const args = node.arguments;
if ( args.length > 0 && isString(args[0]) ) {
const name = ModuleName.fromUI5LegacyName( args[0].value );
if ( nModuleDeclarations === 1 ) {
// if this is the first declaration, then this is the main module declaration
// note that this overrides an already given name
info.name = name;
/* NODE-TODO
info.description = getDocumentation(node);
*/
} else if ( nModuleDeclarations > 1 && name === info.name ) {
// ignore duplicate declarations (e.g. in behavior file of design time controls)
log.warn(`duplicate declaration of module ${getLocation(args)} in ${name}`);
} else {
// otherwise it is just a submodule declaration
info.addSubModule(name);
}
} else {
log.error("jQuery.sap.declare: module name could not be determined from first argument:", args[0]);
}
}
function onDefine(defineCall) {
const args = defineCall.arguments;
const nArgs = args.length;
let i = 0;
// determine the name of the module
let name = defaultName;
if ( i < nArgs && isString(args[i]) ) {
name = ModuleName.fromRequireJSName( args[i++].value );
}
if ( name == null ) {
throw new TypeError("define/sap.ui.define: module name could not be determined," +
`neither from environment nor from first argument: ${args[i] && args[i].type}`);
}
if ( nModuleDeclarations === 1 ) {
// if this is the first declaration, then this is the main module declaration
info.name = name;
// get the documentation from a preceding comment
/* NODE-TODO
info.description = getDocumentation(defineNode);
*/
} else {
if ( name === defaultName ) {
throw new Error("module name could not be determined");
}
info.addSubModule(name);
}
// process array of required modules, if given
if ( i < nArgs && args[i].type === Syntax.ArrayExpression ) {
analyzeDependencyArray(args[i].elements, false, name); // TODO not always false, depends on context?
i++;
}
}
function onJQuerySapRequire(requireCall, conditional) {
const args = requireCall.arguments;
const nArgs = args.length;
if ( nArgs > 0 && args[0].type == Syntax.OBJECT ) {
log.verbose("jQuery.sap.require: cannot evaluate complex require (view/controller)");
} else {
// UI5 signature with one or many required modules
for (let i = 0; i < nArgs; i++) {
const arg = args[i];
if ( isString(arg) ) {
const requiredModuleName = ModuleName.fromUI5LegacyName( arg.value );
info.addDependency(requiredModuleName, conditional);
} else if ( arg.type == Syntax.ConditionalExpression
&& isString(arg.consequent) && isString(arg.alternate) ) {
const requiredModuleName1 = ModuleName.fromUI5LegacyName( arg.consequent.value );
info.addDependency(requiredModuleName1, true);
const requiredModuleName2 = ModuleName.fromUI5LegacyName( arg.alternate.value );
info.addDependency(requiredModuleName2, true);
} else {
log.verbose("jQuery.sap.require: cannot evaluate dynamic arguments: ", arg);
info.dynamicDependencies = true;
}
}
}
}
function onSapUiRequireSync(node, conditional) {
const args = node.arguments;
const nArgs = args.length;
const i = 0;
if ( i < nArgs && isString(args[i]) ) {
const moduleName = ModuleName.fromRequireJSName( args[i].value );
info.addDependency(moduleName, conditional);
}
}
function onSapUiPredefine(predefineCall) {
const args = predefineCall.arguments;
const nArgs = args.length;
let i = 0;
// determine the name of the module
if ( i < nArgs && isString(args[i]) ) {
const moduleName = ModuleName.fromRequireJSName( args[i++].value );
info.addSubModule(moduleName);
} else {
log.warn("sap.ui.predefine call is missing a module name (ignored)");
}
}
function onRegisterPreloadedModules(node, legacyCall) {
const args = node.arguments;
// trace.debug("**** registerPreloadedModules detected");
if ( args.length > 0 && args[0].type == Syntax.ObjectExpression ) {
let modules = args[0];
let isNewSyntax = true;
if ( legacyCall ) {
const obj = args[0];
isNewSyntax = false;
const version = findOwnProperty(obj, "version");
if ( version && isString(version) && parseFloat(version.value) >= 2.0 ) {
isNewSyntax = true;
}
modules = findOwnProperty(obj, "modules");
}
if ( modules && modules.type == Syntax.ObjectExpression ) {
modules.properties.forEach( function(property) {
let moduleName = getPropertyKey(property);
if ( !isNewSyntax ) {
moduleName = ModuleName.fromUI5LegacyName(moduleName);
}
info.addSubModule(moduleName);
});
} else {
log.warn("Cannot evaluate registerPreloadedModules: '%s'", modules && modules.type);
}
}
}
function analyzeDependencyArray(array, conditional, name) {
// console.log(array);
array.forEach( (item) => {
if ( isString(item) ) {
let requiredModule;
if (name == null) {
requiredModule = ModuleName.fromRequireJSName( item.value );
} else {
requiredModule = ModuleName.resolveRelativeRequireJSName(name, item.value);
}
info.addDependency( requiredModule, conditional );
}
});
}
/*
private static final boolean isRealWhitespace(Token t) {
return t.type == Syntax.WhiteSpace || t.type == Syntax.EOL;
}
private String getDocumentation(Tree node) {
int afterWS = node.getTokenStartIndex();
// ignore any real whitespace (WS or EOL)
while ( afterWS > 0 && isRealWhitespace(doc.getToken(afterWS-1)) )
afterWS--;
if (afterWS > 0) {
if ( doc.getToken(afterWS-1).type == Syntax.MultiLineComment ) {
Token t = doc.getToken(afterWS-1);
String s = t.getText();
s = s.replaceFirst("^/\\*[ \t*]*", "");
s = s.replaceAll("[ \t*]*\\* /$", "");
s = s.replaceAll("[\r\n]+[ \t]*\\*+[ \t]*", " ");
s = s.trim();
return s;
} else {
int firstWS = afterWS;
while ( firstWS > 0 && doc.getToken(firstWS-1).type == Syntax.SingleLineComment )
firstWS--;
if( firstWS < afterWS ) {
StringBuilder buf = new StringBuilder();
for (int i=firstWS; i<afterWS; i++) {
Token t = doc.getToken(i);
buf.append(t.getText().substring(2).trim());
}
return buf.toString();
}
}
}
return null;
}
*/
}
}
module.exports = JSModuleAnalyzer;