diff --git a/lib/lbt/analyzer/FioriElementsAnalyzer.js b/lib/lbt/analyzer/FioriElementsAnalyzer.js index 3b6a026ac..bdb2dd7f7 100644 --- a/lib/lbt/analyzer/FioriElementsAnalyzer.js +++ b/lib/lbt/analyzer/FioriElementsAnalyzer.js @@ -61,7 +61,7 @@ const ModuleName = require("../utils/ModuleName"); const SapUiDefine = require("../calls/SapUiDefine"); const {parseJS, Syntax} = require("../utils/parseUtils"); -const {getValue, isMethodCall, isString} = require("../utils/ASTUtils"); +const {getValue, isMethodCall, getStringValue} = require("../utils/ASTUtils"); const log = require("@ui5/logger").getLogger("lbt:analyzer:FioriElementAnalyzer"); // --------------------------------------------------------------------------------------------------------- @@ -165,24 +165,35 @@ class FioriElementsAnalyzer { const TA = defineCall.findImportName("sap/fe/core/TemplateAssembler.js"); // console.log("local name for TemplateAssembler: %s", TA); if ( TA && defineCall.factory ) { - defineCall.factory.body.body.forEach( (stmt) => { - if ( stmt.type === Syntax.ReturnStatement && - isMethodCall(stmt.argument, [TA, "getTemplateComponent"]) && - stmt.argument.arguments.length > 2 && - stmt.argument.arguments[2].type === "ObjectExpression" ) { - templateName = this._analyzeTemplateClassDefinition(stmt.argument.arguments[2]) || templateName; + if (defineCall.factory.type === Syntax.ArrowFunctionExpression && + defineCall.factory.expression === true) { + if ( this._isTemplateClassDefinition(TA, defineCall.factory.body) ) { + templateName = + this._analyzeTemplateClassDefinition(defineCall.factory.body.arguments[2]) || templateName; } - }); + } else { + defineCall.factory.body.body.forEach( (stmt) => { + if ( stmt.type === Syntax.ReturnStatement && + this._isTemplateClassDefinition(TA, stmt.argument) + ) { + templateName = + this._analyzeTemplateClassDefinition(stmt.argument.arguments[2]) || templateName; + } + }); + } } } return templateName; } + _isTemplateClassDefinition(TA, node) { + return isMethodCall(node, [TA, "getTemplateComponent"]) && + node.arguments.length > 2 && + node.arguments[2].type === "ObjectExpression"; + } + _analyzeTemplateClassDefinition(clazz) { - const defaultValue = getValue(clazz, ["metadata", "properties", "templateName", "defaultValue"]); - if ( isString(defaultValue) ) { - return defaultValue.value; - } + return getStringValue(getValue(clazz, ["metadata", "properties", "templateName", "defaultValue"])); } } diff --git a/lib/lbt/analyzer/JSModuleAnalyzer.js b/lib/lbt/analyzer/JSModuleAnalyzer.js index c7053b3b0..c8f4d158b 100644 --- a/lib/lbt/analyzer/JSModuleAnalyzer.js +++ b/lib/lbt/analyzer/JSModuleAnalyzer.js @@ -5,7 +5,9 @@ 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 { + findOwnProperty, getLocation, getPropertyKey, + isMethodCall, isString, getStringValue} = require("../utils/ASTUtils"); const log = require("@ui5/logger").getLogger("lbt:analyzer:JSModuleAnalyzer"); // ------------------------------------------------------------------------------------------------------------------ @@ -64,7 +66,7 @@ const EnrichedVisitorKeys = (function() { BreakStatement: [], CallExpression: [], // special handling CatchClause: ["param", "body"], - ChainExpression: [], + ChainExpression: ["expression"], ClassBody: [], ClassDeclaration: [], ClassExpression: [], @@ -114,7 +116,7 @@ const EnrichedVisitorKeys = (function() { ImportSpecifier: [], // imported, local Literal: [], LabeledStatement: [], - LogicalExpression: [], + LogicalExpression: ["right"], MemberExpression: [], MetaProperty: toBeDone(["meta", "property"]), MethodDefinition: [], @@ -541,21 +543,27 @@ class JSModuleAnalyzer { function onDeclare(node) { const args = node.arguments; - if ( args.length > 0 && isString(args[0]) ) { - const name = ModuleName.fromUI5LegacyName( args[0].value ); - if ( nModuleDeclarations === 1 && !mainModuleFound) { - // if this is the first declaration, then this is the main module declaration - // note that this overrides an already given name - setMainModuleInfo(name, 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 name at ${getLocation(args)} in ${name}`); + if (args.length > 0) { + const value = getStringValue(args[0]); + if (value !== undefined) { + const name = ModuleName.fromUI5LegacyName(value); + if ( nModuleDeclarations === 1 && !mainModuleFound) { + // if this is the first declaration, then this is the main module declaration + // note that this overrides an already given name + setMainModuleInfo(name, 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 name at ${getLocation(args)} in ${name}`); + } else { + // otherwise it is just a submodule declaration + info.addSubModule(name); + } + return; } else { - // otherwise it is just a submodule declaration - info.addSubModule(name); + log.error("jQuery.sap.declare: module name could not be determined from first argument:", args[0]); } } else { - log.error("jQuery.sap.declare: module name could not be determined from first argument:", args[0]); + log.error("jQuery.sap.declare: module name could not be determined, no arguments are given"); } } @@ -569,20 +577,26 @@ class JSModuleAnalyzer { // determine the name of the module let name = null; - if ( i < nArgs && isString(args[i]) ) { - name = ModuleName.fromRequireJSName( args[i++].value ); - if ( name === defaultName ) { - // hardcoded name equals the file name, so this definition qualifies as main module definition - setMainModuleInfo(name, desc); - } else { - info.addSubModule(name); - if ( candidateName == null ) { - // remember the name and description in case no other module qualifies as main module - candidateName = name; - candidateDescription = desc; + if ( i < nArgs ) { + const value = getStringValue( args[i] ); + if ( value !== undefined ) { + name = ModuleName.fromRequireJSName(value); + if ( name === defaultName ) { + // hardcoded name equals the file name, so this definition qualifies as main module definition + setMainModuleInfo(name, desc); + } else { + info.addSubModule(name); + if ( candidateName == null ) { + // remember the name and description in case no other module qualifies as main module + candidateName = name; + candidateDescription = desc; + } } + i++; } - } else { + } + + if ( !name ) { nUnnamedDefines++; if ( nUnnamedDefines > 1 ) { throw new Error( @@ -614,15 +628,25 @@ class JSModuleAnalyzer { // 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 ); + const value = getStringValue(arg); + if ( value !== undefined ) { + const requiredModuleName = ModuleName.fromUI5LegacyName( 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 if ( arg.type == Syntax.ConditionalExpression) { + const consequentValue = getStringValue(arg.consequent); + const alternateValue = getStringValue(arg.alternate); + if ( consequentValue !== undefined ) { + const requiredModuleName1 = ModuleName.fromUI5LegacyName( consequentValue ); + info.addDependency(requiredModuleName1, true); + } + if ( alternateValue !== undefined ) { + const requiredModuleName2 = ModuleName.fromUI5LegacyName( alternateValue ); + info.addDependency(requiredModuleName2, true); + } + if ( consequentValue === undefined || alternateValue === undefined ) { + log.verbose("jQuery.sap.require: cannot evaluate dynamic arguments: ", arg && arg.type); + info.dynamicDependencies = true; + } } else { log.verbose("jQuery.sap.require: cannot evaluate dynamic arguments: ", arg && arg.type); info.dynamicDependencies = true; @@ -637,9 +661,10 @@ class JSModuleAnalyzer { const i = 0; if ( i < nArgs ) { - if ( isString(args[i]) ) { + const value = getStringValue(args[i]); + if ( value !== undefined ) { // sap.ui.requireSync does not support relative dependencies - const moduleName = ModuleName.fromRequireJSName( args[i].value ); + const moduleName = ModuleName.fromRequireJSName( value ); info.addDependency(moduleName, conditional); } else { log.verbose("sap.ui.requireSync: cannot evaluate dynamic arguments: ", args[i] && args[i].type); @@ -654,18 +679,24 @@ class JSModuleAnalyzer { 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); - - // add dependencies - // to correctly identify dependencies e.g. of a library-preload - const elementArg = args[i++]; - if (elementArg && elementArg.type === Syntax.ArrayExpression) { - elementArg.elements.forEach((element) => { - const dependencyName = ModuleName.resolveRelativeRequireJSName(moduleName, element.value); - info.addDependency(dependencyName, conditional); - }); + if ( i < nArgs ) { + const value = getStringValue(args[i++]); + if ( value !== undefined ) { + const moduleName = ModuleName.fromRequireJSName( value ); + info.addSubModule(moduleName); + + // add dependencies + // to correctly identify dependencies e.g. of a library-preload + const elementArg = args[i++]; + if (elementArg && elementArg.type === Syntax.ArrayExpression) { + elementArg.elements.forEach((element) => { + const dependencyName = ModuleName.resolveRelativeRequireJSName(moduleName, + getStringValue(element)); + info.addDependency(dependencyName, conditional); + }); + } + } else { + log.warn("sap.ui.predefine call has a non supported type for module name (ignored)"); } } else { log.warn("sap.ui.predefine call is missing a module name (ignored)"); @@ -692,6 +723,9 @@ class JSModuleAnalyzer { if ( modules && modules.type == Syntax.ObjectExpression ) { modules.properties.forEach( function(property) { let moduleName = getPropertyKey(property); + if ( !moduleName ) { + return; + } if ( namesUseLegacyNotation ) { moduleName = ModuleName.fromUI5LegacyName(moduleName); } @@ -705,16 +739,17 @@ class JSModuleAnalyzer { function analyzeDependencyArray(array, conditional, name) { // console.log(array); array.forEach( (item) => { - if ( isString(item) ) { + const value = getStringValue(item); + if ( value !== undefined ) { // ignore special AMD dependencies (require, exports, module) - if ( SPECIAL_AMD_DEPENDENCIES.indexOf(item.value) >= 0 ) { + if ( SPECIAL_AMD_DEPENDENCIES.indexOf(value) >= 0 ) { return; } let requiredModule; if (name == null) { - requiredModule = ModuleName.fromRequireJSName( item.value ); + requiredModule = ModuleName.fromRequireJSName( value ); } else { - requiredModule = ModuleName.resolveRelativeRequireJSName(name, item.value); + requiredModule = ModuleName.resolveRelativeRequireJSName(name, value); } info.addDependency( requiredModule, conditional ); } else { diff --git a/lib/lbt/analyzer/SmartTemplateAnalyzer.js b/lib/lbt/analyzer/SmartTemplateAnalyzer.js index 1254e1f21..8ba28215e 100644 --- a/lib/lbt/analyzer/SmartTemplateAnalyzer.js +++ b/lib/lbt/analyzer/SmartTemplateAnalyzer.js @@ -28,7 +28,7 @@ const ModuleName = require("../utils/ModuleName"); const SapUiDefine = require("../calls/SapUiDefine"); const {parseJS, Syntax} = require("../utils/parseUtils"); -const {getValue, isMethodCall, isString} = require("../utils/ASTUtils"); +const {getValue, isMethodCall, getStringValue} = require("../utils/ASTUtils"); const log = require("@ui5/logger").getLogger("lbt:analyzer:SmartTemplateAnalyzer"); // --------------------------------------------------------------------------------------------------------- @@ -134,24 +134,35 @@ class TemplateComponentAnalyzer { const TA = defineCall.findImportName("sap/suite/ui/generic/template/lib/TemplateAssembler.js"); // console.log("local name for TemplateAssembler: %s", TA); if ( TA && defineCall.factory ) { - defineCall.factory.body.body.forEach( (stmt) => { - if ( stmt.type === Syntax.ReturnStatement && - isMethodCall(stmt.argument, [TA, "getTemplateComponent"]) && - stmt.argument.arguments.length > 2 && - stmt.argument.arguments[2].type === "ObjectExpression" ) { - templateName = this._analyzeTemplateClassDefinition(stmt.argument.arguments[2]) || templateName; + if (defineCall.factory.type === Syntax.ArrowFunctionExpression && + defineCall.factory.expression === true) { + if ( this._isTemplateClassDefinition(TA, defineCall.factory.body) ) { + templateName = + this._analyzeTemplateClassDefinition(defineCall.factory.body.arguments[2]) || templateName; } - }); + } else { + defineCall.factory.body.body.forEach( (stmt) => { + if ( stmt.type === Syntax.ReturnStatement && + this._isTemplateClassDefinition(TA, stmt.argument) + ) { + templateName = + this._analyzeTemplateClassDefinition(stmt.argument.arguments[2]) || templateName; + } + }); + } } } return templateName; } + _isTemplateClassDefinition(TA, node) { + return isMethodCall(node, [TA, "getTemplateComponent"]) && + node.arguments.length > 2 && + node.arguments[2].type === "ObjectExpression"; + } + _analyzeTemplateClassDefinition(clazz) { - const defaultValue = getValue(clazz, ["metadata", "properties", "templateName", "defaultValue"]); - if ( isString(defaultValue) ) { - return defaultValue.value; - } + return getStringValue(getValue(clazz, ["metadata", "properties", "templateName", "defaultValue"])); } } diff --git a/lib/lbt/analyzer/XMLCompositeAnalyzer.js b/lib/lbt/analyzer/XMLCompositeAnalyzer.js index 0c413bc56..a35cd6bae 100644 --- a/lib/lbt/analyzer/XMLCompositeAnalyzer.js +++ b/lib/lbt/analyzer/XMLCompositeAnalyzer.js @@ -2,7 +2,7 @@ const {Syntax} = require("../utils/parseUtils"); const SapUiDefine = require("../calls/SapUiDefine"); -const {getValue, isMethodCall, isString} = require("../utils/ASTUtils"); +const {getValue, isMethodCall, getStringValue} = require("../utils/ASTUtils"); const ModuleName = require("../utils/ModuleName"); const log = require("@ui5/logger").getLogger("lbt:analyzer:XMLCompositeAnalyzer"); @@ -19,18 +19,30 @@ class XMLCompositeAnalyzer { const XMLC = defineCall.findImportName("sap/ui/core/XMLComposite.js"); // console.log("local name for XMLComposite: %s", XMLC); if ( XMLC && defineCall.factory ) { - defineCall.factory.body.body.forEach( (stmt) => { - if ( stmt.type === Syntax.VariableDeclaration ) { - stmt.declarations.forEach( (decl) => { - fragmentName = this._checkForXMLCClassDefinition( XMLC, decl.init ) || fragmentName; - }); - } else if ( stmt.type === Syntax.ExpressionStatement && - stmt.expression.type === Syntax.AssignmentExpression ) { - fragmentName = this._checkForXMLCClassDefinition( XMLC, stmt.expression.right ) || fragmentName; - } - }); - if ( fragmentName ) { - const fragmentModule = ModuleName.fromUI5LegacyName( fragmentName, ".control.xml" ); + if (defineCall.factory.type === Syntax.ArrowFunctionExpression && + defineCall.factory.expression === true) { + fragmentName = this._checkForXMLCClassDefinition(XMLC, defineCall.factory.body); + } else { + defineCall.factory.body.body.forEach((stmt) => { + if (stmt.type === Syntax.VariableDeclaration) { + stmt.declarations.forEach((decl) => { + fragmentName = this._checkForXMLCClassDefinition(XMLC, decl.init) || fragmentName; + }); + } else if ( + stmt.type === Syntax.ReturnStatement && + ( stmt?.argument?.type === Syntax.CallExpression && stmt.argument.arguments?.length > 1 && + stmt.argument.arguments[1].type === Syntax.ObjectExpression)) { + fragmentName = + this._checkForXMLCClassDefinition(XMLC, stmt.argument) || fragmentName; + } else if (stmt.type === Syntax.ExpressionStatement && + stmt.expression.type === Syntax.AssignmentExpression) { + fragmentName = + this._checkForXMLCClassDefinition(XMLC, stmt.expression.right) || fragmentName; + } + }); + } + if (fragmentName) { + const fragmentModule = ModuleName.fromUI5LegacyName(fragmentName, ".control.xml"); log.verbose("fragment control: add dependency to template fragment %s", fragmentModule); info.addDependency(fragmentModule); } @@ -42,8 +54,9 @@ class XMLCompositeAnalyzer { let fragmentName; if ( isMethodCall(stmt, [XMLC, "extend"]) ) { // log.verbose(stmt); - if ( stmt.arguments.length > 0 && isString(stmt.arguments[0]) ) { - fragmentName = stmt.arguments[0].value; + const value = getStringValue(stmt.arguments[0]); + if ( stmt.arguments.length > 0 && value ) { + fragmentName = value; } if ( stmt.arguments.length > 1 && stmt.arguments[1].type === Syntax.ObjectExpression ) { fragmentName = this._analyzeXMLCClassDefinition(stmt.arguments[1]) || fragmentName; @@ -54,10 +67,7 @@ class XMLCompositeAnalyzer { _analyzeXMLCClassDefinition(clazz) { // log.verbose(clazz); - const fragmentName = getValue(clazz, ["fragment"]); - if ( isString(fragmentName) ) { - return fragmentName.value; - } + return getStringValue(getValue(clazz, ["fragment"])); } } diff --git a/lib/lbt/analyzer/analyzeLibraryJS.js b/lib/lbt/analyzer/analyzeLibraryJS.js index 58955addc..85f36b520 100644 --- a/lib/lbt/analyzer/analyzeLibraryJS.js +++ b/lib/lbt/analyzer/analyzeLibraryJS.js @@ -1,6 +1,7 @@ "use strict"; const {parseJS, Syntax, VisitorKeys} = require("../utils/parseUtils"); const {getPropertyKey, isMethodCall, isIdentifier, getStringArray} = require("../utils/ASTUtils"); +const log = require("@ui5/logger").getLogger("lbt:analyzer:LibraryJS"); const CALL__SAP_UI_GETCORE = ["sap", "ui", "getCore"]; @@ -25,8 +26,14 @@ async function analyze(resource) { node.arguments.length === 1 && node.arguments[0].type === Syntax.ObjectExpression ) { node.arguments[0].properties.forEach( (prop) => { + if (prop.type === Syntax.SpreadElement) { + // SpreadElements are currently not supported + return; + } + const key = getPropertyKey(prop); const value = prop.value; + if ( key === "noLibraryCSS" && (value.type === Syntax.Literal && typeof value.value === "boolean") ) { libInfo.noLibraryCSS = value.value; @@ -38,6 +45,14 @@ async function analyze(resource) { libInfo.controls = getStringArray(value, true); } else if ( key === "elements" && value.type == Syntax.ArrayExpression ) { libInfo.elements = getStringArray(value, true); + } else if ( ["designtime", "dependencies", "extensions", "name", "version"].includes(key) ) { + // do nothing, for all other supported properties + } else { + log.error( + "Unexpected property '" + key + + "' or wrong type for '" + key + + "' in sap.ui.getCore().initLibrary call in '" + resource.getPath() + "'" + ); } }); diff --git a/lib/lbt/bundle/Builder.js b/lib/lbt/bundle/Builder.js index b4e0fc004..2110b725b 100644 --- a/lib/lbt/bundle/Builder.js +++ b/lib/lbt/bundle/Builder.js @@ -648,7 +648,7 @@ async function rewriteDefine({moduleName, moduleContent, moduleSourceMap}) { // Inject module name if missing if ( defineCall.arguments.length == 0 || - defineCall.arguments[0].type !== Syntax.Literal ) { + ![Syntax.Literal, Syntax.TemplateLiteral].includes(defineCall.arguments[0].type)) { let value = `"${ModuleName.toRequireJSName(moduleName)}"`; let index; diff --git a/lib/lbt/calls/SapUiDefine.js b/lib/lbt/calls/SapUiDefine.js index c1b08986c..f2126da60 100644 --- a/lib/lbt/calls/SapUiDefine.js +++ b/lib/lbt/calls/SapUiDefine.js @@ -2,7 +2,7 @@ const {Syntax} = require("../utils/parseUtils"); const ModuleName = require("../utils/ModuleName"); -const {isString, isBoolean} = require("../utils/ASTUtils"); +const {isBoolean, getStringValue} = require("../utils/ASTUtils"); class SapUiDefineCall { constructor(node, moduleName) { @@ -11,6 +11,7 @@ class SapUiDefineCall { this.dependencyArray = null; this.factory = null; this.exportAsGlobal = false; + this.paramNames = null; const args = node.arguments; if ( args == null ) { @@ -27,28 +28,33 @@ class SapUiDefineCall { let i = 0; let params; - if ( i < args.length && isString(args[i]) ) { + const name = getStringValue(args[i]); + if ( i < args.length && name ) { // assert(String) - this.name = args[i++].value; + this.name = name; + i++; } if ( i < args.length && args[i].type === Syntax.ArrayExpression ) { this.dependencyArray = args[i++]; this.dependencies = this.dependencyArray.elements.map( (elem) => { - if ( !isString(elem) ) { + const value = getStringValue(elem); + if ( !value ) { throw new TypeError(); } - return ModuleName.resolveRelativeRequireJSName(this.name, elem.value); + return ModuleName.resolveRelativeRequireJSName(this.name, value); }); this.dependencyInsertionIdx = this.dependencyArray.elements.length; } - if ( i < args.length && args[i].type === Syntax.FunctionExpression ) { + if ( i < args.length && ( + args[i].type === Syntax.FunctionExpression || args[i].type === Syntax.ArrowFunctionExpression) + ) { this.factory = args[i++]; params = this.factory.params; this.paramNames = params.map( (param) => { if ( param.type !== Syntax.Identifier ) { - throw new TypeError(); + return null; } return param.name; }); diff --git a/lib/lbt/utils/ASTUtils.js b/lib/lbt/utils/ASTUtils.js index c95772460..5fc5db31a 100644 --- a/lib/lbt/utils/ASTUtils.js +++ b/lib/lbt/utils/ASTUtils.js @@ -13,10 +13,31 @@ const {Syntax} = require("../utils/parseUtils"); * @returns {boolean} Whether the node is a literal and whether its value matches the given string */ function isString(node, literal) { - if ( node == null || node.type !== Syntax.Literal || typeof node.value !== "string" ) { + const value = getStringValue(node); + if (value === undefined) { return false; } - return literal == null ? true : node.value === literal; + return literal == null ? true: value === literal; +} + +function getStringValue(node) { + if (isLiteral(node)) { + return node.value; + } else if (isTemplateLiteralWithoutExpression(node)) { + return node?.quasis?.[0]?.value?.cooked; + } else { + return undefined; + } +} + +function isLiteral(node) { + return node && node.type === Syntax.Literal && typeof node.value === "string"; +} + +function isTemplateLiteralWithoutExpression(node) { + return node?.type === Syntax.TemplateLiteral && + node?.expressions?.length === 0 && + node?.quasis?.length === 1; } function isBoolean(node, literal) { @@ -47,27 +68,27 @@ function isNamedObject(node, objectPath, length) { } function isIdentifier(node, name) { - if ( node.type != Syntax.Identifier ) { - return false; - } - if ( typeof name == "string" ) { + if ( node.type === Syntax.Identifier && typeof name == "string" ) { return name === node.name; + } else if ( node.type === Syntax.Identifier && Array.isArray(name) ) { + return name.find((name) => name === node.name || name === "*") !== undefined; + } else if ( node.type === Syntax.ObjectPattern ) { + return node.properties.filter((childnode) => isIdentifier(childnode.key, name)).length > 0; + } else if ( node.type === Syntax.ArrayPattern ) { + return node.elements.filter((childnode) => isIdentifier(childnode, name)).length > 0; + } else { + return false; } - for (let i = 0; i < name.length; i++) { - if ( name[i] === node.name || name[i] === "*" ) { - return true; - } - } - return false; } function getPropertyKey(property) { - if ( property.key.type === Syntax.Identifier ) { + if ( property.type === Syntax.SpreadElement ) { + // TODO: Support interpreting SpreadElements + return; + } else if ( property.key.type === Syntax.Identifier && property.computed !== true ) { return property.key.name; } else if ( property.key.type === Syntax.Literal ) { return String(property.key.value); - } else { - throw new Error(); } } @@ -102,10 +123,15 @@ function getValue(obj, names) { */ function getStringArray(array, skipNonStringLiterals) { return array.elements.reduce( (result, item) => { - if ( isString(item) ) { - result.push(item.value); + const value = getStringValue(item); + if ( value !== undefined ) { + result.push(value); } else if ( !skipNonStringLiterals ) { - throw new TypeError("array element is not a string literal:" + item.type); + if (item.type === Syntax.TemplateLiteral) { + throw new TypeError("array element is a template literal with expressions"); + } else { + throw new TypeError("array element is not a string literal: " + item.type); + } } return result; }, []); @@ -113,6 +139,7 @@ function getStringArray(array, skipNonStringLiterals) { module.exports = { isString, + getStringValue, isBoolean, isMethodCall, isNamedObject, diff --git a/test/fixtures/lbt/modules/declare_dynamic_require_conditional.js b/test/fixtures/lbt/modules/declare_dynamic_require_conditional.js new file mode 100644 index 000000000..9849ee03b --- /dev/null +++ b/test/fixtures/lbt/modules/declare_dynamic_require_conditional.js @@ -0,0 +1,3 @@ +jQuery.sap.declare("sap.ui.testmodule"); +const Foo = "conditional/module2"; +jQuery.sap.require(true ? "conditional/module1" : Foo); diff --git a/test/fixtures/lbt/modules/declare_require_conditional.js b/test/fixtures/lbt/modules/declare_require_conditional.js new file mode 100644 index 000000000..7ba08c7b5 --- /dev/null +++ b/test/fixtures/lbt/modules/declare_require_conditional.js @@ -0,0 +1,2 @@ +jQuery.sap.declare("sap.ui.testmodule"); +jQuery.sap.require(true ? `conditional/module1` : "conditional/module2"); diff --git a/test/fixtures/lbt/modules/es6-async-module.js b/test/fixtures/lbt/modules/es6-async-module.js new file mode 100644 index 000000000..b8c9f786a --- /dev/null +++ b/test/fixtures/lbt/modules/es6-async-module.js @@ -0,0 +1,5 @@ +// async function with await +sap.ui.define([], async () => { + return await sap.ui.require(["static/module1"], async () => {}); + // await and sap.ui.require, makes no sense because it does NOT return a promise but it should still be analyzed +}); diff --git a/test/fixtures/lbt/modules/es6-syntax-dynamic-dependencies.js b/test/fixtures/lbt/modules/es6-syntax-dynamic-dependencies.js new file mode 100644 index 000000000..73ce63cd7 --- /dev/null +++ b/test/fixtures/lbt/modules/es6-syntax-dynamic-dependencies.js @@ -0,0 +1,7 @@ +sap.ui.define([ + 'static/module1' +], () => { + // spread expression, currently not detected as dependency + const dynamicModules = ["not-detected/module1"]; + sap.ui.require(["static/module1", ...dynamicModules]); +}); diff --git a/test/fixtures/lbt/modules/es6-syntax.js b/test/fixtures/lbt/modules/es6-syntax.js index 2f9924594..14f27715c 100644 --- a/test/fixtures/lbt/modules/es6-syntax.js +++ b/test/fixtures/lbt/modules/es6-syntax.js @@ -3,14 +3,13 @@ sap.ui.define([ ], (m1) => { // using an arrow function for the module factory sap.ui.require(['static/module2'], function() { - sap.ui.require(['static/module3'], function() {}); + sap.ui.require(["static/module3"], function() {}); sap.ui.require('no-dependency/module1'); // probing API does not introduce a dependency }); // using an arrow function for the require callback sap.ui.require([], () => { - sap.ui.require(['static/module4'], function() { - }); + sap.ui.require(['static/module4'], () => sap.ui.require(['static/module8'])); }); // default value in array destructuring @@ -41,8 +40,56 @@ sap.ui.define([ }; // chain expression - if (m1?.foo?.bar) { - sap.ui.require(["conditional/module4"]); + sap?.ui?.require(["conditional/module4"]); + + // iterator pattern + const iterator = { + [Symbol.iterator]() { + return { + next () { + sap.ui.require(['conditional/module6']); + return { done: true, value: 1 }; + } + }; + } + } + + // generator pattern + let generator = { + *[Symbol.iterator]() { + for (;;) { + yield sap.ui.require(['conditional/module7']); + } + } } + // class declaration + class Clz { + + * bar () { + sap.ui.require(['conditional/module8']); + } + + get conditionalModule() { sap.ui.require(['conditional/module9']) } + + static foo () { + sap.ui.require(['conditional/module10']) + } + + }; + + m1 ?? sap.ui.require(['conditional/module11']); + + // ObjectPattern as Id + const {module11, module12} = { + module11: sap.ui.require(['static/module11'], function(){}), + module12: sap.ui.require(['static/module12'], function(){}) + }; + + // ArrayPattern as Id + const [module13, module14] = [ + sap.ui.require(['static/module13'], function(){}), + sap.ui.require(['static/module14'], function(){}) + ]; + }); diff --git a/test/fixtures/lbt/modules/es6-template-literal-predefine.js b/test/fixtures/lbt/modules/es6-template-literal-predefine.js new file mode 100644 index 000000000..ddc8dccac --- /dev/null +++ b/test/fixtures/lbt/modules/es6-template-literal-predefine.js @@ -0,0 +1,4 @@ +// template literal as module name +sap.ui.predefine(`mypath/mymodule`, [`static/module1`, "static/module2" ], () => { + return sap.ui.require([`static/module3`], () => { }); // template literal when loading module +}); diff --git a/test/fixtures/lbt/modules/es6-template-literal-with-expression.js b/test/fixtures/lbt/modules/es6-template-literal-with-expression.js new file mode 100644 index 000000000..1cd360471 --- /dev/null +++ b/test/fixtures/lbt/modules/es6-template-literal-with-expression.js @@ -0,0 +1,8 @@ +// template literal as module name +sap.ui.define(`mypath/mymodule`, [`static/module1`, "static/module2" ], () => { + const i = 4; + return sap.ui.require([ + `static/module3`, + `not-detected/module${i}` // template literal with expression when loading module, not detected by the JSModuleAnalayzer + ], () => { }); +}); diff --git a/test/fixtures/lbt/modules/es6-template-literal.js b/test/fixtures/lbt/modules/es6-template-literal.js new file mode 100644 index 000000000..8fcdeb3de --- /dev/null +++ b/test/fixtures/lbt/modules/es6-template-literal.js @@ -0,0 +1,4 @@ +// template literal as module name +sap.ui.define(`mypath/mymodule`, [`static/module1`, "static/module2" ], () => { + return sap.ui.require([`static/module3`], () => { }); // template literal when loading module +}); diff --git a/test/lib/lbt/analyzer/FioriElementsAnalyzer.js b/test/lib/lbt/analyzer/FioriElementsAnalyzer.js index d63a36ee2..3b68529ec 100644 --- a/test/lib/lbt/analyzer/FioriElementsAnalyzer.js +++ b/test/lib/lbt/analyzer/FioriElementsAnalyzer.js @@ -1,7 +1,27 @@ const test = require("ava"); const FioriElementsAnalyzer = require("../../../../lib/lbt/analyzer/FioriElementsAnalyzer"); -const sinon = require("sinon"); const parseUtils = require("../../../../lib/lbt/utils/parseUtils"); +const sinonGlobal = require("sinon"); +const logger = require("@ui5/logger"); +const loggerInstance = logger.getLogger(); +const mock = require("mock-require"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + mock.stopAll(); +}); + +function setupFioriElementsAnalyzerWithStubbedLogger({context}) { + const {sinon} = context; + context.warningLogSpy = sinon.spy(loggerInstance, "warn"); + sinon.stub(logger, "getLogger").returns(loggerInstance); + context.FioriElementsAnalyzerWithStubbedLogger = + mock.reRequire("../../../../lib/lbt/analyzer/FioriElementsAnalyzer"); +} test("analyze: with Component.js", async (t) => { const emptyPool = {}; @@ -27,7 +47,7 @@ test("analyze: without manifest", async (t) => { const analyzer = new FioriElementsAnalyzer(mockPool); - const stubAnalyzeManifest = sinon.stub(analyzer, "_analyzeManifest").resolves(); + const stubAnalyzeManifest = t.context.sinon.stub(analyzer, "_analyzeManifest").resolves(); const name = "MyComponent.js"; const result = await analyzer.analyze({name}, moduleInfo); @@ -60,7 +80,7 @@ test("analyze: with manifest", async (t) => { const analyzer = new FioriElementsAnalyzer(mockPool); - const stubAnalyzeManifest = sinon.stub(analyzer, "_analyzeManifest").resolves(); + const stubAnalyzeManifest = t.context.sinon.stub(analyzer, "_analyzeManifest").resolves(); const name = "MyComponent.js"; await analyzer.analyze({name}, moduleInfo); @@ -86,11 +106,11 @@ test("_analyzeManifest: Manifest with TemplateAssembler code", async (t) => { const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const analyzer = new FioriElementsAnalyzer(); - const stubAnalyzeTemplateComponent = sinon.stub(analyzer, "_analyzeTemplateComponent").resolves(); + const stubAnalyzeTemplateComponent = t.context.sinon.stub(analyzer, "_analyzeTemplateComponent").resolves(); await analyzer._analyzeManifest(manifest, moduleInfo); @@ -130,7 +150,7 @@ test.serial("_analyzeTemplateComponent: Manifest with TemplateAssembler code", a const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const mockPool = { async findResource() { @@ -142,8 +162,8 @@ test.serial("_analyzeTemplateComponent: Manifest with TemplateAssembler code", a const analyzer = new FioriElementsAnalyzer(mockPool); - const stubAnalyzeAST = sinon.stub(analyzer, "_analyzeAST").returns("mytpl"); - const stubParse = sinon.stub(parseUtils, "parseJS").returns(""); + const stubAnalyzeAST = t.context.sinon.stub(analyzer, "_analyzeAST").returns("mytpl"); + const stubParse = t.context.sinon.stub(parseUtils, "parseJS").returns(""); await analyzer._analyzeTemplateComponent("pony", {}, moduleInfo); @@ -164,7 +184,7 @@ test.serial("_analyzeTemplateComponent: no default template name", async (t) => const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const mockPool = { async findResource() { @@ -176,8 +196,8 @@ test.serial("_analyzeTemplateComponent: no default template name", async (t) => const analyzer = new FioriElementsAnalyzer(mockPool); - const stubAnalyzeAST = sinon.stub(analyzer, "_analyzeAST").returns(""); - const stubParse = sinon.stub(parseUtils, "parseJS").returns(""); + const stubAnalyzeAST = t.context.sinon.stub(analyzer, "_analyzeAST").returns(""); + const stubParse = t.context.sinon.stub(parseUtils, "parseJS").returns(""); await analyzer._analyzeTemplateComponent("pony", {}, moduleInfo); @@ -193,7 +213,7 @@ test.serial("_analyzeTemplateComponent: with template name from pageConfig", asy const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const mockPool = { async findResource() { @@ -205,8 +225,8 @@ test.serial("_analyzeTemplateComponent: with template name from pageConfig", asy const analyzer = new FioriElementsAnalyzer(mockPool); - const stubAnalyzeAST = sinon.stub(analyzer, "_analyzeAST").returns(""); - const stubParse = sinon.stub(parseUtils, "parseJS").returns(""); + const stubAnalyzeAST = t.context.sinon.stub(analyzer, "_analyzeAST").returns(""); + const stubParse = t.context.sinon.stub(parseUtils, "parseJS").returns(""); await analyzer._analyzeTemplateComponent("pony", { component: { @@ -225,7 +245,7 @@ test.serial("_analyzeTemplateComponent: with template name from pageConfig", asy stubParse.restore(); }); -test("_analyzeAST: get template name from ast", async (t) => { +test("_analyzeAST: get template name from ast", (t) => { const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], function(a, TemplateAssembler){ return TemplateAssembler.getTemplateComponent(getMethods, "sap.fe.templates.Page.Component", { @@ -240,22 +260,239 @@ test("_analyzeAST: get template name from ast", async (t) => { } });});`; const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); +test("_analyzeAST: get template name from ast (AMD define)", (t) => { + const code = `define(["a", "sap/fe/core/TemplateAssembler"], function(a, TemplateAssembler){ + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); - const stubAnalyzeTemplateClassDefinition = sinon.stub(analyzer, - "_analyzeTemplateClassDefinition").returns("donkey"); +test("_analyzeAST: unable to get template name from ast (no TemplateAssembler import)", (t) => { + const code = `sap.ui.define(["a"], // import missing + function(a, TemplateAssembler){ + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, ""); +}); - const result = await analyzer._analyzeAST("pony", ast); +test("_analyzeAST: unable to get template name from ast (no module definition)", (t) => { + const code = `myDefine(["a", "sap/fe/core/TemplateAssembler"], // unsupported module definition + function(a, TemplateAssembler){ + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, ""); +}); +test("_analyzeAST: unable to get template name from ast (ArrowFunction with implicit return #1)", (t) => { + const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], + (a, TemplateAssembler) => TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + // No templateName provided + "manifest": "json" + } + }));`; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, ""); +}); - t.true(stubAnalyzeTemplateClassDefinition.calledOnce, "_analyzeTemplateClassDefinition was called once"); +test("_analyzeAST: unable to get template name from ast (ArrowFunction with implicit return #2)", (t) => { + const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], + (a, TemplateAssembler) => TemplateAssembler.extend(getMethods, // wrong call. should be 'getTemplateComponent' + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + }));`; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, ""); +}); - stubAnalyzeTemplateClassDefinition.restore(); - t.is(result, "donkey"); +test.serial("_analyzeAST: get template name from ast (async function)", (t) => { + const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], async function(a, TemplateAssembler){ + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + setupFioriElementsAnalyzerWithStubbedLogger(t); + const {FioriElementsAnalyzerWithStubbedLogger} = t.context; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzerWithStubbedLogger(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test.serial("_analyzeAST: get template name from ast (async ArrowFunction)", (t) => { + const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], async (a, TemplateAssembler) => { + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + setupFioriElementsAnalyzerWithStubbedLogger(t); + const {FioriElementsAnalyzerWithStubbedLogger} = t.context; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzerWithStubbedLogger(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test.serial("_analyzeAST: get template name from ast (async ArrowFunction with implicit return)", (t) => { + const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], + async (a, TemplateAssembler) => TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + }));`; + setupFioriElementsAnalyzerWithStubbedLogger(t); + const {FioriElementsAnalyzerWithStubbedLogger} = t.context; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzerWithStubbedLogger(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); }); -test("_analyzeAST: no template name from ast", async (t) => { +test("_analyzeAST: get template name from ast (ArrowFunction)", (t) => { + const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], (a, TemplateAssembler) => { + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test("_analyzeAST: get template name from ast (ArrowFunction with implicit return)", (t) => { + const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], + (a, TemplateAssembler) => TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + }));`; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test("_analyzeAST: get template name from ast (with SpreadElement)", (t) => { + const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], (a, TemplateAssembler) => { + const myTemplate = { + templateName: { + type: "string", + defaultValue: "sap.fe.templates.Page.view.Page" + } + }; + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + ...myTemplate + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + const analyzer = new FioriElementsAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + + t.is(templateName, "", "The TemplateName is correctly empty as SpreadElements are not supported"); +}); + +test("_analyzeAST: no template name from ast", (t) => { const code = `sap.ui.define(["a", "sap/fe/core/TemplateAssembler"], function(a, TemplateAssembler){ return TemplateAssembler.getTemplateComponent(getMethods, "sap.fe.templates.Page.Component", { @@ -273,11 +510,10 @@ test("_analyzeAST: no template name from ast", async (t) => { const analyzer = new FioriElementsAnalyzer(); - const stubAnalyzeTemplateClassDefinition = sinon.stub(analyzer, + const stubAnalyzeTemplateClassDefinition = t.context.sinon.stub(analyzer, "_analyzeTemplateClassDefinition").returns(false); - const result = await analyzer._analyzeAST("pony", ast); - + const result = analyzer._analyzeAST("pony", ast); t.true(stubAnalyzeTemplateClassDefinition.calledOnce, "_analyzeTemplateClassDefinition was called once"); diff --git a/test/lib/lbt/analyzer/JSModuleAnalyzer.js b/test/lib/lbt/analyzer/JSModuleAnalyzer.js index fe263ab60..052cec880 100644 --- a/test/lib/lbt/analyzer/JSModuleAnalyzer.js +++ b/test/lib/lbt/analyzer/JSModuleAnalyzer.js @@ -492,34 +492,229 @@ test("Bundle", async (t) => { test("ES6 Syntax", async (t) => { const info = await analyze("modules/es6-syntax.js", "modules/es6-syntax.js"); + const expected = [ "conditional/module1.js", + "conditional/module10.js", + "conditional/module11.js", "conditional/module2.js", "conditional/module3.js", "conditional/module4.js", + "conditional/module6.js", + "conditional/module7.js", + "conditional/module8.js", + "conditional/module9.js", "static/module1.js", + "static/module11.js", + "static/module12.js", + "static/module13.js", + "static/module14.js", "static/module2.js", "static/module3.js", "static/module4.js", "static/module5.js", "static/module6.js", "static/module7.js", + "static/module8.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"); + `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"); + `all dependencies other than 'conditional/*' and 'static/*' should be implicit (${dep})`); t.false(info.dynamicDependencies, - "no use of dynamic dependencies should have been detected"); + `no use of dynamic dependencies should have been detected (${dep})`); + t.false(info.rawModule, + `ui5 module (${dep})`); + }); +}); + +test("ES6 Syntax (with dynamic dependencies)", async (t) => { + const info = await analyze( + "modules/es6-syntax-dynamic-dependencies.js", + "modules/es6-syntax-dynamic-dependencies.js"); + const expected = [ + "static/module1.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.true(info.dynamicDependencies, + `use of dynamic dependencies should have been detected (${dep})`); + t.false(info.rawModule, + `ui5 module (${dep})`); + }); +}); + +test("ES6 Async Module", async (t) => { + const info = await analyze("modules/es6-async-module.js", "modules/es6-async-module.js"); + const expected = [ + "static/module1.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 (${dep})`); + t.false(info.rawModule, + `ui5 module (${dep})`); + }); +}); + +test("ES6 Template Literal", async (t) => { + const info = await analyze("modules/es6-template-literal.js", "modules/es6-template-literal.js"); + const expected = [ + "static/module1.js", + "static/module2.js", + "static/module3.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 (${dep})`); t.false(info.rawModule, - "ui5 module"); + `ui5 module (${dep})`); }); }); +test("ES6 Template Literal with Expression", async (t) => { + const info = await analyze("modules/es6-template-literal-with-expression.js", + "modules/es6-template-literal-with-expression.js"); + const expected = [ + "static/module1.js", + "static/module2.js", + "static/module3.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.true(info.dynamicDependencies, + `use of dynamic dependencies should have been detected (${dep})`); + t.false(info.rawModule, + `ui5 module (${dep})`); + }); +}); + +test("ES6 Template Literal in sap.ui.predefine", async (t) => { + const info = await analyze("modules/es6-template-literal-predefine.js", + "modules/es6-template-literal-predefine.js"); + const expected = [ + "static/module1.js", + "static/module2.js", + "static/module3.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 (${dep})`); + t.false(info.rawModule, + `ui5 module (${dep})`); + }); +}); + +test("ChainExpression", (t) => { + const content = ` + sap.ui.define(['require', 'static/module1'], (require) => { + sap?.ui?.require?.(['conditional/module2']); + sap?.ui?.requireSync?.('conditional/module3'); + jQuery?.sap?.require?.('conditional.module4'); + require?.(['conditional/module5']); + });`; + const info = analyzeString(content, "modules/ChainExpression.js"); + + const expected = [ + "conditional/module2.js", + "conditional/module3.js", + "conditional/module4.js", + "conditional/module5.js", + "jquery.sap.global.js", + "static/module1.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("LogicalExpression", (t) => { + const content = ` + sap.ui.define(['require', 'static/module1'], (require, module1) => { + module1 && sap.ui.require(['conditional/module2']); + module1 || sap.ui.requireSync('conditional/module3'); + module1 ?? jQuery.sap.require('conditional.module4'); + !module1 && require(['conditional/module5']); + + sap.ui.require(['static/module2']) && module1; + sap.ui.requireSync('static/module3') || module1; + jQuery.sap.require('static.module4') ?? module1; + require(['static/module5']) && module1; + });`; + const info = analyzeString(content, "modules/LogicalExpression.js"); + + const expected = [ + "conditional/module2.js", + "conditional/module3.js", + "conditional/module4.js", + "conditional/module5.js", + "jquery.sap.global.js", + "static/module1.js", + "static/module2.js", + "static/module3.js", + "static/module4.js", + "static/module5.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, @@ -528,6 +723,49 @@ test("Dynamic import (declare/require)", async (t) => { "ui5 module"); }); +test("Conditional import (declare/require)", async (t) => { + const info = await analyze("modules/declare_require_conditional.js", + "modules/declare_require_conditional.js"); + const expected = [ + "conditional/module1.js", + "conditional/module2.js", + "jquery.sap.global.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 (${dep})`); + t.false(info.rawModule, + `ui5 module (${dep})`); + }); +}); + +test("Dynamic import (declare/require/conditional)", async (t) => { + const info = await analyze("modules/declare_dynamic_require_conditional.js", + "modules/declare_dynamic_require_conditional.js"); + const expected = [ + "conditional/module1.js", + "jquery.sap.global.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.true(info.dynamicDependencies, + `use of dynamic dependencies should have been detected (${dep})`); + t.false(info.rawModule, + `ui5 module (${dep})`); + }); +}); + test("Dynamic import (define/require)", async (t) => { const info = await analyze("modules/amd_dynamic_require.js"); t.true(info.dynamicDependencies, @@ -670,6 +908,20 @@ jQuery.sap.registerPreloadedModules({ "submodule from jQuery.sap.registerPreloadedModules"); }); +test("jQuery.sap.registerPreloadedModules (with ObjectExpression, version 1.0 and SpreadExpression)", (t) => { + const content = ` +jQuery.sap.registerPreloadedModules({ + "modules": { + ...foo + }, + "version": "1.0" +}); +`; + const info = analyzeString(content, "modules/registerPreloadedModules-ObjectExpression.js"); + t.deepEqual(info.subModules, [], + "submodule from jQuery.sap.registerPreloadedModules are empty"); +}); + test("jQuery.sap.registerPreloadedModules (with ObjectExpression, version 2.0)", (t) => { const content = ` jQuery.sap.registerPreloadedModules({ @@ -684,6 +936,20 @@ jQuery.sap.registerPreloadedModules({ "submodule from jQuery.sap.registerPreloadedModules"); }); +test("jQuery.sap.registerPreloadedModules (with ObjectExpression, version 2.0 and SpreadExpression)", (t) => { + const content = ` +jQuery.sap.registerPreloadedModules({ + "modules": { + ...foo + }, + "version": "2.0" +}); +`; + const info = analyzeString(content, "modules/registerPreloadedModules-ObjectExpression.js"); + t.deepEqual(info.subModules, [], + "submodule from jQuery.sap.registerPreloadedModules are empty"); +}); + test("Module that contains jQuery.sap.declare should be derived as subModule", (t) => { const content = ` sap.ui.define([], function() { diff --git a/test/lib/lbt/analyzer/SmartTemplateAnalyzer.js b/test/lib/lbt/analyzer/SmartTemplateAnalyzer.js index 4dc4d129a..37f65a527 100644 --- a/test/lib/lbt/analyzer/SmartTemplateAnalyzer.js +++ b/test/lib/lbt/analyzer/SmartTemplateAnalyzer.js @@ -1,9 +1,28 @@ const test = require("ava"); const SmartTemplateAnalyzer = require("../../../../lib/lbt/analyzer/SmartTemplateAnalyzer"); const ModuleInfo = require("../../../../lib/lbt/resources/ModuleInfo"); -const sinon = require("sinon"); const parseUtils = require("../../../../lib/lbt/utils/parseUtils"); +const sinonGlobal = require("sinon"); +const logger = require("@ui5/logger"); +const loggerInstance = logger.getLogger(); +const mock = require("mock-require"); +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + mock.stopAll(); +}); + +function setupSmartTemplateAnalyzerWithStubbedLogger({context}) { + const {sinon} = context; + context.warningLogSpy = sinon.spy(loggerInstance, "warn"); + sinon.stub(logger, "getLogger").returns(loggerInstance); + context.SmartTemplateAnalyzerWithStubbedLogger = + mock.reRequire("../../../../lib/lbt/analyzer/SmartTemplateAnalyzer"); +} test("analyze: with Component.js", async (t) => { const emptyPool = {}; @@ -28,7 +47,7 @@ test("analyze: without manifest", async (t) => { const moduleInfo = {}; const analyzer = new SmartTemplateAnalyzer(mockPool); - const stubAnalyzeManifest = sinon.stub(analyzer, "_analyzeManifest").resolves(); + const stubAnalyzeManifest = t.context.sinon.stub(analyzer, "_analyzeManifest").resolves(); const name = "MyComponent.js"; const result = await analyzer.analyze({name}, moduleInfo); @@ -52,7 +71,7 @@ test("_analyzeManifest: with manifest with recursive pages (as array)", async (t const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const manifest = { "sap.ui.generic.app": { @@ -76,7 +95,7 @@ test("_analyzeManifest: with manifest with recursive pages (as array)", async (t }; const analyzer = new SmartTemplateAnalyzer(); - const stubAnalyzeTemplateComponent = sinon.stub(analyzer, "_analyzeTemplateComponent").resolves(); + const stubAnalyzeTemplateComponent = t.context.sinon.stub(analyzer, "_analyzeTemplateComponent").resolves(); await analyzer._analyzeManifest(manifest, moduleInfo); @@ -120,7 +139,7 @@ test("_analyzeManifest: with manifest with recursive pages (as object)", async ( const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const manifest = { "sap.ui.generic.app": { @@ -148,7 +167,7 @@ test("_analyzeManifest: with manifest with recursive pages (as object)", async ( }; const analyzer = new SmartTemplateAnalyzer(); - const stubAnalyzeTemplateComponent = sinon.stub(analyzer, "_analyzeTemplateComponent").resolves(); + const stubAnalyzeTemplateComponent = t.context.sinon.stub(analyzer, "_analyzeTemplateComponent").resolves(); await analyzer._analyzeManifest(manifest, moduleInfo); @@ -194,7 +213,7 @@ test.serial("_analyzeTemplateComponent: Manifest with TemplateAssembler code", a const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const mockPool = { async findResource() { @@ -209,8 +228,8 @@ test.serial("_analyzeTemplateComponent: Manifest with TemplateAssembler code", a const analyzer = new SmartTemplateAnalyzer(mockPool); - const stubAnalyzeAST = sinon.stub(analyzer, "_analyzeAST").returns("mytpl"); - const stubParse = sinon.stub(parseUtils, "parseJS").returns(""); + const stubAnalyzeAST = t.context.sinon.stub(analyzer, "_analyzeAST").returns("mytpl"); + const stubParse = t.context.sinon.stub(parseUtils, "parseJS").returns(""); await analyzer._analyzeTemplateComponent("pony", {}, moduleInfo); @@ -231,7 +250,7 @@ test.serial("_analyzeTemplateComponent: no default template name", async (t) => const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const mockPool = { async findResource() { @@ -246,8 +265,8 @@ test.serial("_analyzeTemplateComponent: no default template name", async (t) => const analyzer = new SmartTemplateAnalyzer(mockPool); - const stubAnalyzeAST = sinon.stub(analyzer, "_analyzeAST").returns(""); - const stubParse = sinon.stub(parseUtils, "parseJS").returns(""); + const stubAnalyzeAST = t.context.sinon.stub(analyzer, "_analyzeAST").returns(""); + const stubParse = t.context.sinon.stub(parseUtils, "parseJS").returns(""); await analyzer._analyzeTemplateComponent("pony", {}, moduleInfo); @@ -263,7 +282,7 @@ test.serial("_analyzeTemplateComponent: with template name from pageConfig", asy const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const mockPool = { async findResource() { @@ -278,8 +297,8 @@ test.serial("_analyzeTemplateComponent: with template name from pageConfig", asy const analyzer = new SmartTemplateAnalyzer(mockPool); - const stubAnalyzeAST = sinon.stub(analyzer, "_analyzeAST").returns(""); - const stubParse = sinon.stub(parseUtils, "parseJS").returns(""); + const stubAnalyzeAST = t.context.sinon.stub(analyzer, "_analyzeAST").returns(""); + const stubParse = t.context.sinon.stub(parseUtils, "parseJS").returns(""); await analyzer._analyzeTemplateComponent("pony", { component: { @@ -302,7 +321,7 @@ test.serial("_analyzeTemplateComponent: dependency not found", async (t) => { const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const mockPool = { async findResource() { @@ -315,8 +334,8 @@ test.serial("_analyzeTemplateComponent: dependency not found", async (t) => { const analyzer = new SmartTemplateAnalyzer(mockPool); - const stubAnalyzeAST = sinon.stub(analyzer, "_analyzeAST").returns(""); - const stubParse = sinon.stub(parseUtils, "parseJS").returns(""); + const stubAnalyzeAST = t.context.sinon.stub(analyzer, "_analyzeAST").returns(""); + const stubParse = t.context.sinon.stub(parseUtils, "parseJS").returns(""); const error = await t.throwsAsync(analyzer._analyzeTemplateComponent("pony", { component: { @@ -339,7 +358,7 @@ test.serial("_analyzeTemplateComponent: dependency not found is ignored", async const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const mockPool = { async findResource() { @@ -352,8 +371,8 @@ test.serial("_analyzeTemplateComponent: dependency not found is ignored", async const analyzer = new SmartTemplateAnalyzer(mockPool); - const stubAnalyzeAST = sinon.stub(analyzer, "_analyzeAST").returns(""); - const stubParse = sinon.stub(parseUtils, "parseJS").returns(""); + const stubAnalyzeAST = t.context.sinon.stub(analyzer, "_analyzeAST").returns(""); + const stubParse = t.context.sinon.stub(parseUtils, "parseJS").returns(""); await analyzer._analyzeTemplateComponent("pony", { component: { @@ -370,7 +389,7 @@ test.serial("_analyzeTemplateComponent: dependency not found is ignored", async stubParse.restore(); }); -test("_analyzeAST: get template name from ast", async (t) => { +test("_analyzeAST: get template name from ast", (t) => { const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], function(a, TemplateAssembler){ return TemplateAssembler.getTemplateComponent(getMethods, @@ -389,10 +408,10 @@ test("_analyzeAST: get template name from ast", async (t) => { const analyzer = new SmartTemplateAnalyzer(); - const stubAnalyzeTemplateClassDefinition = sinon.stub(analyzer, + const stubAnalyzeTemplateClassDefinition = t.context.sinon.stub(analyzer, "_analyzeTemplateClassDefinition").returns("donkey"); - const result = await analyzer._analyzeAST("pony", ast); + const result = analyzer._analyzeAST("pony", ast); t.true(stubAnalyzeTemplateClassDefinition.calledOnce, "_analyzeTemplateClassDefinition was called once"); @@ -401,7 +420,251 @@ test("_analyzeAST: get template name from ast", async (t) => { t.is(result, "donkey"); }); -test("_analyzeAST: no template name from ast", async (t) => { +test("_analyzeAST: get template name from ast (AMD define)", (t) => { + const code = `define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], function(a, TemplateAssembler) { + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test("_analyzeAST: unable to get template name from ast (no TemplateAssembler import)", (t) => { + const code = `define(["a"], function(a, TemplateAssembler) { // import missing + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, ""); +}); + +test("_analyzeAST: unable to get template name from ast (no module definition)", (t) => { + const code = `myDefine(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + function(a, TemplateAssembler) { // unsupported module definition + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, ""); +}); + +test("_analyzeAST: get template name from ast (ArrowFunction)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + (a, TemplateAssembler) => { + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + + const analyzer = new SmartTemplateAnalyzer(); + + const stubAnalyzeTemplateClassDefinition = t.context.sinon.stub(analyzer, + "_analyzeTemplateClassDefinition").returns("donkey"); + + const result = analyzer._analyzeAST("pony", ast); + + + t.true(stubAnalyzeTemplateClassDefinition.calledOnce, "_analyzeTemplateClassDefinition was called once"); + + stubAnalyzeTemplateClassDefinition.restore(); + t.is(result, "donkey"); +}); + +test("_analyzeAST: get template name from ast (ArrowFunction with implicit return)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + (a, TemplateAssembler) => TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + }));`; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test.serial("_analyzeAST: get template name from ast (async factory function)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + async function (a, TemplateAssembler) { + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + } + );});`; + setupSmartTemplateAnalyzerWithStubbedLogger(t); + const {SmartTemplateAnalyzerWithStubbedLogger} = t.context; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzerWithStubbedLogger(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test("_analyzeAST: unable to get template name from ast (ArrowFunction with implicit return #1)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + (a, TemplateAssembler) => TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + // No templateName provided + "manifest": "json" + } + }));`; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, ""); +}); + +test("_analyzeAST: unable to get template name from ast (ArrowFunction with implicit return #2)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + (a, TemplateAssembler) => TemplateAssembler.extend(getMethods, // wrong call. should be 'getTemplateComponent' + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + }));`; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, ""); +}); + +test.serial("_analyzeAST: get template name from ast (async arrow factory function)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + async (a, TemplateAssembler) => { + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + } + );});`; + setupSmartTemplateAnalyzerWithStubbedLogger(t); + const {SmartTemplateAnalyzerWithStubbedLogger} = t.context; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzerWithStubbedLogger(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test.serial("_analyzeAST: get template name from ast (async arrow factory function implicit return)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + async (a, TemplateAssembler) => TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": "sap.fe.templates.Page.view.Page" + } + }, + "manifest": "json" + } + } + ));`; + setupSmartTemplateAnalyzerWithStubbedLogger(t); + const {SmartTemplateAnalyzerWithStubbedLogger} = t.context; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzerWithStubbedLogger(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page"); +}); + +test("_analyzeAST: get template name from ast (with SpreadElement)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + (a, TemplateAssembler) => { + const myTemplate = { + templateName: { + type: "string", + defaultValue: "sap.fe.templates.Page.view.Page" + } + }; + return TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + ...myTemplate + }, + "manifest": "json" + } + });});`; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + + t.is(templateName, "", "The TemplateName is correctly empty as SpreadElements are not supported"); +}); + + +test("_analyzeAST: no template name from ast", (t) => { const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], function(a, TemplateAssembler){ return TemplateAssembler.getTemplateComponent(getMethods, @@ -420,10 +683,10 @@ test("_analyzeAST: no template name from ast", async (t) => { const analyzer = new SmartTemplateAnalyzer(); - const stubAnalyzeTemplateClassDefinition = sinon.stub(analyzer, + const stubAnalyzeTemplateClassDefinition = t.context.sinon.stub(analyzer, "_analyzeTemplateClassDefinition").returns(false); - const result = await analyzer._analyzeAST("pony", ast); + const result = analyzer._analyzeAST("pony", ast); t.true(stubAnalyzeTemplateClassDefinition.calledOnce, "_analyzeTemplateClassDefinition was called once"); @@ -432,6 +695,26 @@ test("_analyzeAST: no template name from ast", async (t) => { t.is(result, ""); }); +test("_analyzeAST: get template name (template literal)", (t) => { + const code = `sap.ui.define(["a", "sap/suite/ui/generic/template/lib/TemplateAssembler"], + (a, TemplateAssembler) => TemplateAssembler.getTemplateComponent(getMethods, + "sap.fe.templates.Page.Component", { + metadata: { + properties: { + "templateName": { + "type": "string", + "defaultValue": \`sap.fe.templates.Page.view.Page\` + } + }, + "manifest": "json" + } + }));`; + const ast = parseUtils.parseJS(code); + const analyzer = new SmartTemplateAnalyzer(); + const templateName = analyzer._analyzeAST("sap.fe.templates.Page.Component", ast); + t.is(templateName, "sap.fe.templates.Page.view.Page", "The TemplateName is correct"); +}); + test("Analysis of Manifest and TemplateAssembler code", async (t) => { const manifest = { "sap.ui.generic.app": { diff --git a/test/lib/lbt/analyzer/XMLCompositeAnalyzer.js b/test/lib/lbt/analyzer/XMLCompositeAnalyzer.js index ef8820756..75f61de6f 100644 --- a/test/lib/lbt/analyzer/XMLCompositeAnalyzer.js +++ b/test/lib/lbt/analyzer/XMLCompositeAnalyzer.js @@ -1,10 +1,30 @@ const test = require("ava"); const {parseJS} = require("../../../../lib/lbt/utils/parseUtils"); -const sinon = require("sinon"); const XMLCompositeAnalyzer = require("../../../../lib/lbt/analyzer/XMLCompositeAnalyzer"); const ModuleInfo = require("../../../../lib/lbt/resources/ModuleInfo"); +const sinonGlobal = require("sinon"); +const logger = require("@ui5/logger"); +const loggerInstance = logger.getLogger(); +const mock = require("mock-require"); -test("integration: XMLComposite code", async (t) => { +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + mock.stopAll(); +}); + +function setupXMLCompositeAnalyzerWithStubbedLogger({context}) { + const {sinon} = context; + context.warningLogSpy = sinon.spy(loggerInstance, "warn"); + sinon.stub(logger, "getLogger").returns(loggerInstance); + context.XMLCompositeAnalyzerWithStubbedLogger = + mock.reRequire("../../../../lib/lbt/analyzer/XMLCompositeAnalyzer"); +} + +test("integration: XMLComposite code with VariableDeclaration", (t) => { const code = `sap.ui.define([ 'jquery.sap.global', 'sap/ui/core/XMLComposite'], function(jQuery, XMLComposite) { @@ -18,12 +38,137 @@ test("integration: XMLComposite code", async (t) => { const analyzer = new XMLCompositeAnalyzer(); const name = "composites.ButtonList"; const moduleInfo = new ModuleInfo(); - await analyzer.analyze(ast, name, moduleInfo); + analyzer.analyze(ast, name, moduleInfo); t.deepEqual(moduleInfo.dependencies, ["composites/ButtonList.control.xml"], - "Dependency should be created from component name"); + "Dependency should be created from composite name"); }); -test("analyze: not an XMLComposite module", async (t) => { +test("integration: XMLComposite code", (t) => { + const code = `sap.ui.define([ + 'jquery.sap.global', 'sap/ui/core/XMLComposite'], + function(jQuery, XMLComposite) { + "use strict"; + return XMLComposite.extend("composites.ButtonList", {}); + });`; + + const ast = parseJS(code); + + const analyzer = new XMLCompositeAnalyzer(); + const name = "composites.ButtonList"; + const moduleInfo = new ModuleInfo(); + analyzer.analyze(ast, name, moduleInfo); + t.deepEqual(moduleInfo.dependencies, ["composites/ButtonList.control.xml"], + "Dependency should be created from composite name"); +}); + +test("integration: XMLComposite code (arrow factory function)", (t) => { + const code = `sap.ui.define([ + 'jquery.sap.global', 'sap/ui/core/XMLComposite'], + (jQuery, XMLComposite) => { + return XMLComposite.extend("composites.ButtonList", {}); + });`; + + const ast = parseJS(code); + + const analyzer = new XMLCompositeAnalyzer(); + const name = "composites.ButtonList"; + const moduleInfo = new ModuleInfo(); + analyzer.analyze(ast, name, moduleInfo); + t.deepEqual(moduleInfo.dependencies, ["composites/ButtonList.control.xml"], + "Dependency should be created from composite name"); +}); + +test("integration: XMLComposite code (arrow factory function with implicit return)", (t) => { + const code = `sap.ui.define([ + 'jquery.sap.global', 'sap/ui/core/XMLComposite'], + (jQuery, XMLComposite) => XMLComposite.extend("composites.ButtonList", {}));`; + + const ast = parseJS(code); + + const analyzer = new XMLCompositeAnalyzer(); + const name = "composites.ButtonList"; + const moduleInfo = new ModuleInfo(); + analyzer.analyze(ast, name, moduleInfo); + t.deepEqual(moduleInfo.dependencies, ["composites/ButtonList.control.xml"], + "Dependency should be created from composite name"); +}); + +test.serial("integration: XMLComposite code (async factory function)", (t) => { + const code = `sap.ui.define([ + 'jquery.sap.global', 'sap/ui/core/XMLComposite'], + async function(jQuery, XMLComposite) { + "use strict"; + return XMLComposite.extend("composites.ButtonList", {}); + });`; + setupXMLCompositeAnalyzerWithStubbedLogger(t); + const {XMLCompositeAnalyzerWithStubbedLogger} = t.context; + const ast = parseJS(code); + const analyzer = new XMLCompositeAnalyzerWithStubbedLogger(); + const name = "composites.ButtonList"; + const moduleInfo = new ModuleInfo(); + analyzer.analyze(ast, name, moduleInfo); + t.deepEqual(moduleInfo.dependencies, ["composites/ButtonList.control.xml"], + "Dependency should be created from composite name"); +}); + +test.serial("integration: XMLComposite code (async arrow factory function)", (t) => { + const code = `sap.ui.define([ + 'jquery.sap.global', 'sap/ui/core/XMLComposite'], + async (jQuery, XMLComposite) => { + return XMLComposite.extend("composites.ButtonList", {}); + });`; + setupXMLCompositeAnalyzerWithStubbedLogger(t); + const {XMLCompositeAnalyzerWithStubbedLogger} = t.context; + const ast = parseJS(code); + const analyzer = new XMLCompositeAnalyzerWithStubbedLogger(); + const name = "composites.ButtonList"; + const moduleInfo = new ModuleInfo(); + analyzer.analyze(ast, name, moduleInfo); + t.deepEqual(moduleInfo.dependencies, ["composites/ButtonList.control.xml"], + "Dependency should be created from composite name"); +}); + +test.serial("integration: XMLComposite code (async arrow factory function with implicit return)", (t) => { + const code = `sap.ui.define([ + 'jquery.sap.global', 'sap/ui/core/XMLComposite'], + async (jQuery, XMLComposite) => XMLComposite.extend("composites.ButtonList", {}));`; + setupXMLCompositeAnalyzerWithStubbedLogger(t); + const {XMLCompositeAnalyzerWithStubbedLogger} = t.context; + const ast = parseJS(code); + const analyzer = new XMLCompositeAnalyzerWithStubbedLogger(); + const name = "composites.ButtonList"; + const moduleInfo = new ModuleInfo(); + analyzer.analyze(ast, name, moduleInfo); + t.deepEqual(moduleInfo.dependencies, ["composites/ButtonList.control.xml"], + "Dependency should be created from composite name"); +}); + +test("integration: XMLComposite code with SpreadElement", (t) => { + const code = `sap.ui.define([ + 'jquery.sap.global', 'sap/ui/core/XMLComposite'], + (jQuery, XMLComposite) => { + const myXMLComposite = { + fragment: "composites.custom.ButtonList" + }; + return XMLComposite.extend("composites.ButtonList", { + ...myXMLComposite + }); + });`; + + + const ast = parseJS(code); + + const analyzer = new XMLCompositeAnalyzer(); + const name = "composites.ButtonList"; + const moduleInfo = new ModuleInfo(); + analyzer.analyze(ast, name, moduleInfo); + + t.deepEqual(moduleInfo.dependencies, ["composites/ButtonList.control.xml"], + "Dependency should be created from composite name because overridden by the 'fragment' property " + + " is not possible to lacking SpreadElement support"); +}); + +test("analyze: not an XMLComposite module", (t) => { const code = `sap.ui.define([ 'jquery.sap.global', 'sap/ui/core/XMLComposite'], function(jQuery, XMLComposite) { @@ -36,17 +181,18 @@ test("analyze: not an XMLComposite module", async (t) => { const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const analyzer = new XMLCompositeAnalyzer(); - const stubCheckForXMLCClassDefinition = sinon.stub(analyzer, "_checkForXMLCClassDefinition").returns("cow"); + const stubCheckForXMLCClassDefinition = + t.context.sinon.stub(analyzer, "_checkForXMLCClassDefinition").returns("cow"); const name = "composites.ButtonList"; - await analyzer.analyze(ast, name, moduleInfo); + analyzer.analyze(ast, name, moduleInfo); t.false(stubCheckForXMLCClassDefinition.called, "_checkForXMLCClassDefinition was not called"); t.false(stubAddDependency.called, "addDependency was not called"); }); -test("analyze: XMLComposite VariableDeclaration code", async (t) => { +test("analyze: XMLComposite VariableDeclaration code", (t) => { const code = `sap.ui.define([ 'jquery.sap.global', 'sap/ui/core/XMLComposite'], function(jQuery, XMLComposite) { @@ -60,12 +206,13 @@ test("analyze: XMLComposite VariableDeclaration code", async (t) => { const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const analyzer = new XMLCompositeAnalyzer(); - const stubCheckForXMLCClassDefinition = sinon.stub(analyzer, "_checkForXMLCClassDefinition").returns("cow"); + const stubCheckForXMLCClassDefinition = + t.context.sinon.stub(analyzer, "_checkForXMLCClassDefinition").returns("cow"); const name = "composites.ButtonList"; - await analyzer.analyze(ast, name, moduleInfo); + analyzer.analyze(ast, name, moduleInfo); t.true(stubCheckForXMLCClassDefinition.calledOnce, "_checkForXMLCClassDefinition was called once"); t.is(stubCheckForXMLCClassDefinition.getCall(0).args[0], "XMLComposite", "_checkForXMLCClassDefinition should be called with the name"); @@ -77,7 +224,7 @@ test("analyze: XMLComposite VariableDeclaration code", async (t) => { }); -test("analyze: XMLComposite Expression code", async (t) => { +test("analyze: XMLComposite Expression code", (t) => { const code = `sap.ui.define([ 'jquery.sap.global', 'sap/ui/core/XMLComposite'], function(jQuery, XMLComposite) { @@ -90,12 +237,13 @@ test("analyze: XMLComposite Expression code", async (t) => { const moduleInfo = { addDependency: function() {} }; - const stubAddDependency = sinon.spy(moduleInfo, "addDependency"); + const stubAddDependency = t.context.sinon.spy(moduleInfo, "addDependency"); const analyzer = new XMLCompositeAnalyzer(); - const stubCheckForXMLCClassDefinition = sinon.stub(analyzer, "_checkForXMLCClassDefinition").returns("cow"); + const stubCheckForXMLCClassDefinition = + t.context.sinon.stub(analyzer, "_checkForXMLCClassDefinition").returns("cow"); const name = "composites.ButtonList"; - await analyzer.analyze(ast, name, moduleInfo); + analyzer.analyze(ast, name, moduleInfo); t.true(stubCheckForXMLCClassDefinition.calledOnce, "_checkForXMLCClassDefinition was called once"); t.is(stubCheckForXMLCClassDefinition.getCall(0).args[0], "XMLComposite", "_checkForXMLCClassDefinition should be called with the name"); @@ -112,7 +260,7 @@ test("_checkForXMLCClassDefinition: string argument and object expression", (t) const ast = parseJS(code); const analyzer = new XMLCompositeAnalyzer(); - const stubAnalyzeXMLCClassDefinition = sinon.stub(analyzer, "_analyzeXMLCClassDefinition").returns("cow"); + const stubAnalyzeXMLCClassDefinition = t.context.sinon.stub(analyzer, "_analyzeXMLCClassDefinition").returns("cow"); const result = analyzer._checkForXMLCClassDefinition("XMLComposite", ast.body[0].expression); t.true(stubAnalyzeXMLCClassDefinition.calledOnce, "_checkForXMLCClassDefinition was called once"); @@ -120,6 +268,14 @@ test("_checkForXMLCClassDefinition: string argument and object expression", (t) "addDependency should be called with the dependency name"); }); +test("_checkForXMLCClassDefinition: string argument (template literal)", (t) => { + const code = `XMLComposite.extend(\`composites.ButtonList\`, {})`; + const ast = parseJS(code); + const analyzer = new XMLCompositeAnalyzer(); + const fragmentName = analyzer._checkForXMLCClassDefinition("XMLComposite", ast.body[0].expression); + t.is(fragmentName, "composites.ButtonList"); +}); + test("_analyzeXMLCClassDefinition: name retrieval", (t) => { const code = `test({fragment: "cat"})`; @@ -131,3 +287,15 @@ test("_analyzeXMLCClassDefinition: name retrieval", (t) => { t.is(result, "cat", "addDependency should be called with the dependency name"); }); + +test("_analyzeXMLCClassDefinition: name retrieval (template literal)", (t) => { + const code = `test({fragment: \`cat\`})`; + + const ast = parseJS(code); + + const analyzer = new XMLCompositeAnalyzer(); + const result = analyzer._analyzeXMLCClassDefinition(ast.body[0].expression.arguments[0]); + + t.is(result, "cat", + "addDependency should be called with the dependency name"); +}); diff --git a/test/lib/lbt/analyzer/analyzeLibraryJS.js b/test/lib/lbt/analyzer/analyzeLibraryJS.js new file mode 100644 index 000000000..73c1eb130 --- /dev/null +++ b/test/lib/lbt/analyzer/analyzeLibraryJS.js @@ -0,0 +1,154 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +function createMockResource(content, path) { + return { + async getBuffer() { + return content; + }, + getPath() { + return path; + } + }; +} + +test.afterEach.always((t) => { + mock.stopAll(); + sinon.restore(); +}); + +test.serial("analyze: library.js with non supported property", async (t) => { + const libraryJS = ` +sap.ui.define([ + 'sap/ui/core/Core', +], function(Core) { + "use strict"; + sap.ui.getCore().initLibrary({ + name : "library.test", + version: "1.0.0", + customProperty1: "UI5", + dependencies : ["sap.ui.core"], + types: [ + "library.test.ButtonType", + "library.test.DialogType", + ], + interfaces: [ + "library.test.IContent", + ], + controls: [ + "library.test.Button", + "library.test.CheckBox", + "library.test.Dialog", + "library.test.Input", + "library.test.Label", + "library.test.Link", + "library.test.Menu", + "library.test.Text" + ], + elements: [ + "library.test.MenuItem" + ], + extensions: { + customExtension: "UI5" + }, + designtime: "library/test/library.designtime.js", + customProperty2: "UI5" + }); + return thisLib; +});`; + + const librayJSPath = "library/test/library.js"; + const logger = require("@ui5/logger"); + const errorLogStub = sinon.stub(); + const myLoggerInstance = { + error: errorLogStub + }; + sinon.stub(logger, "getLogger").returns(myLoggerInstance); + const analyzeLibraryJSWithStubbedLogger = mock.reRequire("../../../../lib/lbt/analyzer/analyzeLibraryJS"); + + const mockResource = createMockResource(libraryJS, librayJSPath); + + await analyzeLibraryJSWithStubbedLogger(mockResource); + + t.is(errorLogStub.callCount, 2, "Error log is called twice"); + t.is(errorLogStub.getCall(0).args[0], + "Unexpected property 'customProperty1' or wrong type for 'customProperty1'" + + " in sap.ui.getCore().initLibrary call in 'library/test/library.js'", + "The error log message of the first call is correct"); + t.is(errorLogStub.getCall(1).args[0], + "Unexpected property 'customProperty2' or wrong type for 'customProperty2'" + + " in sap.ui.getCore().initLibrary call in 'library/test/library.js'", + "The error log message of the first call is correct"); +}); + + +test.serial("analyze: library.js with SpreadExpression", async (t) => { + const libraryJS = ` +sap.ui.define([ + 'sap/ui/core/Core', +], function(Core) { + "use strict"; + const myExtensions = {myProperty1: "Value1", myProperty2: "Value2"}; + sap.ui.getCore().initLibrary({ + ...myExtensions, + name : "library.test", + version: "1.0.0", + elements: [ + "library.test.MenuItem" + ], + }); + return thisLib; +});`; + + const librayJSPath = "library/test/library.js"; + const logger = require("@ui5/logger"); + const errorLogStub = sinon.stub(); + const myLoggerInstance = { + error: errorLogStub + }; + sinon.stub(logger, "getLogger").returns(myLoggerInstance); + const analyzeLibraryJSWithStubbedLogger = mock.reRequire("../../../../lib/lbt/analyzer/analyzeLibraryJS"); + + const mockResource = createMockResource(libraryJS, librayJSPath); + + const result = await analyzeLibraryJSWithStubbedLogger(mockResource); + + t.is(errorLogStub.callCount, 0, "Error log is not called"); + t.is(result.elements[0], "library.test.MenuItem", "The libraryjs is correctly analyzed"); +}); + +test.serial("analyze: library.js with property 'noLibraryCSS'", async (t) => { + const libraryJS = ` +sap.ui.define([ + 'sap/ui/core/Core', +], function(Core) { + "use strict"; + sap.ui.getCore().initLibrary({ + name : "library.test", + version: "1.0.0", + noLibraryCSS: true, + elements: [ + "library.test.MenuItem" + ], + }); + return thisLib; +});`; + + const librayJSPath = "library/test/library.js"; + const logger = require("@ui5/logger"); + const errorLogStub = sinon.stub(); + const myLoggerInstance = { + error: errorLogStub + }; + sinon.stub(logger, "getLogger").returns(myLoggerInstance); + const analyzeLibraryJSWithStubbedLogger = mock.reRequire("../../../../lib/lbt/analyzer/analyzeLibraryJS"); + + const mockResource = createMockResource(libraryJS, librayJSPath); + + const result = await analyzeLibraryJSWithStubbedLogger(mockResource); + + t.is(errorLogStub.callCount, 0, "Error log is not called"); + t.is(result.elements[0], "library.test.MenuItem", "The libraryjs is correctly analyzed"); + t.true(result.noLibraryCSS, "The 'noLibraryCSS' property is correctly 'true'"); +}); diff --git a/test/lib/lbt/bundle/Builder.js b/test/lib/lbt/bundle/Builder.js index af4dff4b2..3a9b84aff 100644 --- a/test/lib/lbt/bundle/Builder.js +++ b/test/lib/lbt/bundle/Builder.js @@ -1744,6 +1744,58 @@ test("rewriteDefine (with empty moduleSourceMap)", async (t) => { }); }); +test("rewriteDefine (with same module name)", async (t) => { + const {rewriteDefine} = Builder.__localFunctions__; + + const {moduleContent, moduleSourceMap} = await rewriteDefine({ + moduleName: "my/test/module.js", + moduleContent: "sap.ui.define(\"my/test/module\", [],(()=>1));", + moduleSourceMap: undefined + }); + + t.is(moduleContent, `sap.ui.predefine("my/test/module", [],(()=>1));`); + t.is(moduleSourceMap, undefined); +}); + +test("rewriteDefine (with other module name)", async (t) => { + const {rewriteDefine} = Builder.__localFunctions__; + + const {moduleContent, moduleSourceMap} = await rewriteDefine({ + moduleName: "my/test/module1.js", + moduleContent: "sap.ui.define(\"my/test/module\", [],(()=>1));", + moduleSourceMap: undefined + }); + + t.is(moduleContent, `sap.ui.predefine("my/test/module", [],(()=>1));`); + t.is(moduleSourceMap, undefined); +}); + +test("rewriteDefine (with same module name as template literal)", async (t) => { + const {rewriteDefine} = Builder.__localFunctions__; + + const {moduleContent, moduleSourceMap} = await rewriteDefine({ + moduleName: "my/test/module.js", + moduleContent: "sap.ui.define(`my/test/module`, [],(()=>1));", + moduleSourceMap: undefined + }); + + t.is(moduleContent, "sap.ui.predefine(`my/test/module`, [],(()=>1));"); + t.is(moduleSourceMap, undefined); +}); + +test("rewriteDefine (with other module name as template literal)", async (t) => { + const {rewriteDefine} = Builder.__localFunctions__; + + const {moduleContent, moduleSourceMap} = await rewriteDefine({ + moduleName: "my/test/module1.js", + moduleContent: "sap.ui.define(`my/test/module`, [],(()=>1));", + moduleSourceMap: undefined + }); + + t.is(moduleContent, "sap.ui.predefine(`my/test/module`, [],(()=>1));"); + t.is(moduleSourceMap, undefined); +}); + test("getSourceMapForModule: Source map resource named after module resource (no sourceMappingURL)", async (t) => { const originalSourceMap = { "version": 3, diff --git a/test/lib/lbt/calls/SapUiDefine.js b/test/lib/lbt/calls/SapUiDefine.js index d722a8c9c..d8082eff7 100644 --- a/test/lib/lbt/calls/SapUiDefine.js +++ b/test/lib/lbt/calls/SapUiDefine.js @@ -1,13 +1,32 @@ const test = require("ava"); const {parseJS, Syntax} = require("../../../../lib/lbt/utils/parseUtils"); - const SapUiDefineCall = require("../../../../lib/lbt/calls/SapUiDefine"); +const logger = require("@ui5/logger"); +const loggerInstance = logger.getLogger(); +const sinonGlobal = require("sinon"); +const mock = require("mock-require"); function parse(code) { const ast = parseJS(code); return ast.body[0].expression; } +function setupSapUiDefineCallWithStubbedLogger({context}) { + const {sinon} = context; + context.warningLogSpy = sinon.spy(loggerInstance, "warn"); + sinon.stub(logger, "getLogger").returns(loggerInstance); + context.SapUiDefineCallWithStubbedLogger = mock.reRequire("../../../../lib/lbt/calls/SapUiDefine"); +} + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + mock.stopAll(); +}); + test("Empty Define", (t) => { const ast = parse("sap.ui.define();"); const call = new SapUiDefineCall(ast, "FileSystemName"); @@ -20,6 +39,12 @@ test("Named Define", (t) => { t.is(call.name, "HardcodedName"); }); +test("Named Define (template literal)", (t) => { + const ast = parse("sap.ui.define(`HardcodedName`, [], function() {});"); + const call = new SapUiDefineCall(ast, "FileSystemName"); + t.is(call.name, "HardcodedName"); +}); + test("Unnamed Define", (t) => { const ast = parse("sap.ui.define([], function() {});"); const call = new SapUiDefineCall(ast, "FileSystemName"); @@ -72,6 +97,47 @@ test("Find Import Name (no dependencies)", (t) => { t.is(call.findImportName("wanted.js"), null); }); +test("Find Import Name (template literal)", (t) => { + const ast = parse("sap.ui.define([`wanted`], function(johndoe) {});"); + const call = new SapUiDefineCall(ast, "FileSystemName"); + t.is(call.findImportName("wanted.js"), "johndoe"); +}); + +test("Find Import Name (destructuring)", (t) => { + const ast = parse("sap.ui.define(['invalid', 'wanted', 'invalid1'], function({inv}, johndoe, [inv1]) {});"); + const call = new SapUiDefineCall(ast, "FileSystemName"); + t.is(call.findImportName("invalid.js"), null); + t.is(call.findImportName("wanted.js"), "johndoe"); + t.is(call.findImportName("invalid1.js"), null); +}); + +test.serial("Find Import Name (async function)", (t) => { + setupSapUiDefineCallWithStubbedLogger(t); + const {SapUiDefineCallWithStubbedLogger, warningLogSpy} = t.context; + const ast = parse("sap.ui.define(['wanted'], async function(johndoe) {});"); + const call = new SapUiDefineCallWithStubbedLogger(ast, "FileSystemName"); + t.is(call.findImportName("wanted.js"), "johndoe"); + t.is(warningLogSpy.callCount, 0, "Warning log is not called"); +}); + +test.serial("Find Import Name (async arrow function)", (t) => { + setupSapUiDefineCallWithStubbedLogger(t); + const {SapUiDefineCallWithStubbedLogger, warningLogSpy} = t.context; + const ast = parse("sap.ui.define(['wanted'], async (johndoe) => {return johndoe});"); + const call = new SapUiDefineCallWithStubbedLogger(ast, "FileSystemName"); + t.is(call.findImportName("wanted.js"), "johndoe"); + t.is(warningLogSpy.callCount, 0, "Warning log is not called"); +}); + +test.serial("Find Import Name (async arrow function with implicit return)", (t) => { + setupSapUiDefineCallWithStubbedLogger(t); + const {SapUiDefineCallWithStubbedLogger, warningLogSpy} = t.context; + const ast = parse("sap.ui.define(['wanted'], async (johndoe) => johndoe);"); + const call = new SapUiDefineCallWithStubbedLogger(ast, "FileSystemName"); + t.is(call.findImportName("wanted.js"), "johndoe"); + t.is(warningLogSpy.callCount, 0, "Warning log is not called"); +}); + test("Export as Global: omitted", (t) => { const ast = parse("sap.ui.define(['wanted'], function(johndoe) {});"); const call = new SapUiDefineCall(ast, "FileSystemName"); diff --git a/test/lib/lbt/utils/ASTUtils.js b/test/lib/lbt/utils/ASTUtils.js index 23788c0b4..e63e301fb 100644 --- a/test/lib/lbt/utils/ASTUtils.js +++ b/test/lib/lbt/utils/ASTUtils.js @@ -22,6 +22,18 @@ test("isString", (t) => { t.false(ASTUtils.isString(literal, "myOtherValue47"), "is a literal but its value does not match"); }); +test("isString (template literal)", (t) => { + t.false(ASTUtils.isString(null)); + + const templateliteral = parseJS("`testValue47`").body[0].expression; + + t.true(ASTUtils.isString(templateliteral), "is a template literal"); + t.true(ASTUtils.isString(templateliteral, "testValue47"), "is a template literal and its value matches"); + t.true(ASTUtils.isString(templateliteral, `testValue47`), + "is a template literal and its value matches (template literal)"); + t.false(ASTUtils.isString(templateliteral, "myOtherValue47"), "is a template literal but its value does not match"); +}); + test("isBoolean", (t) => { t.false(ASTUtils.isString(null)); @@ -40,7 +52,7 @@ test("isBoolean", (t) => { t.false(ASTUtils.isBoolean(falseLiteral, true), "is a literal and value does not matches"); }); -test("isIdentifier", (t) => { +test("isIdentifier (identifier)", (t) => { const literal = parseJS("'testValue47'").body[0].expression; t.false(ASTUtils.isIdentifier(literal), "A literal is not an identifier"); @@ -58,6 +70,36 @@ test("isIdentifier", (t) => { t.false(ASTUtils.isIdentifier(identifier, [], "value does not match")); }); +test("isIdentifier (object pattern)", (t) => { + const identifier = parseJS("const { a, b } = { a: 'x', b: 'y' }").body[0].declarations[0].id; + + t.true(ASTUtils.isIdentifier(identifier, ["*"], "asterisk matches any string")); + t.true(ASTUtils.isIdentifier(identifier, ["a"], "value matches")); + t.true(ASTUtils.isIdentifier(identifier, "a"), "value matches"); + t.true(ASTUtils.isIdentifier(identifier, ["b"], "value matches")); + t.true(ASTUtils.isIdentifier(identifier, "b"), "value matches"); + + t.false(ASTUtils.isIdentifier(identifier, ""), "value does not match"); + t.false(ASTUtils.isIdentifier(identifier, "*"), "value does not match"); + t.false(ASTUtils.isIdentifier(identifier, "c"), "value does not match"); + t.false(ASTUtils.isIdentifier(identifier, [], "value does not match")); +}); + +test("isIdentifier (arry pattern)", (t) => { + const identifier = parseJS("const [ a, b ] = [ 'x', 'y' ]").body[0].declarations[0].id; + + t.true(ASTUtils.isIdentifier(identifier, ["*"], "asterisk matches any string")); + t.true(ASTUtils.isIdentifier(identifier, ["a"], "value matches")); + t.true(ASTUtils.isIdentifier(identifier, "a"), "value matches"); + t.true(ASTUtils.isIdentifier(identifier, ["b"], "value matches")); + t.true(ASTUtils.isIdentifier(identifier, "b"), "value matches"); + + t.false(ASTUtils.isIdentifier(identifier, ""), "value does not match"); + t.false(ASTUtils.isIdentifier(identifier, "*"), "value does not match"); + t.false(ASTUtils.isIdentifier(identifier, "c"), "value does not match"); + t.false(ASTUtils.isIdentifier(identifier, [], "value does not match")); +}); + test("isNamedObject", (t) => { const identifier = parseJS("testValue47").body[0].expression; @@ -87,16 +129,36 @@ test("isMethodCall", (t) => { test("getStringArray", (t) => { const array = parseJS("['a', 5]").body[0].expression; - const error = t.throws(() => { + t.throws(() => { ASTUtils.getStringArray(array); - }, {instanceOf: TypeError}, "array contains a number"); - - t.is(error.message, "array element is not a string literal:Literal"); + }, { + instanceOf: TypeError, + message: "array element is not a string literal: Literal" + }, "array contains a number"); const stringArray = parseJS("['a', 'x']").body[0].expression; t.deepEqual(ASTUtils.getStringArray(stringArray), ["a", "x"], "array contains only strings"); }); +test("getStringArray (skipNonStringLiterals=true)", (t) => { + const array = parseJS("['a', `x`, true, 5, `${foo}`, {}]").body[0].expression; + t.deepEqual(ASTUtils.getStringArray(array, true), ["a", "x"], "result contains only strings"); +}); + +test("getStringArray (template literal)", (t) => { + const array = parseJS("[`a`, `${a}`]").body[0].expression; + t.throws(() => { + ASTUtils.getStringArray(array); + }, { + instanceOf: TypeError, + message: "array element is a template literal with expressions" + }); + + const stringArray = parseJS("[`a`, 'x']").body[0].expression; + t.deepEqual(ASTUtils.getStringArray(stringArray), ["a", "x"], + "array contains only strings or template literals without expressions"); +}); + test("getLocation", (t) => { t.is(ASTUtils.getLocation([{value: "module/name"}]), "module/name"); }); @@ -113,10 +175,19 @@ test("getPropertyKey", (t) => { // quoted key with dash const dashedProperties = parseJS("var myVar = {'my-var': 47}").body[0].declarations[0].init.properties; t.is(ASTUtils.getPropertyKey(dashedProperties[0]), "my-var", "sole property key is 'my-var'"); + + // SpreadElement (not supported) + const spreadElement = parseJS("var myVar = { ...foo }").body[0].declarations[0].init.properties; + t.is(ASTUtils.getPropertyKey(spreadElement[0]), undefined); + + // Computed property key (not supported) + const computedKey = parseJS(`var myVar = { ["foo" + "bar"]: 42 }`).body[0].declarations[0].init.properties; + t.is(ASTUtils.getPropertyKey(computedKey[0]), undefined); }); test("findOwnProperty", (t) => { const literal = cleanse(parseJS("'x'").body[0].expression); + const identifier = cleanse(parseJS("a").body[0].expression); // quoted const object = parseJS("var myVar = {'a':'x'}").body[0].declarations[0].init; @@ -125,6 +196,16 @@ test("findOwnProperty", (t) => { // unquoted const object2 = parseJS("var myVar = {a:'x'}").body[0].declarations[0].init; t.deepEqual(cleanse(ASTUtils.findOwnProperty(object2, "a")), literal, "object property a's value is literal 'x'"); + + // number + const object3 = parseJS("var myVar = {3: 'x'}").body[0].declarations[0].init; + t.deepEqual(cleanse(ASTUtils.findOwnProperty(object3, "3")), literal, + "object property 3's value is identifier a"); + + // shorthand identifier + const object4 = parseJS("var myVar = {a}").body[0].declarations[0].init; + t.deepEqual(cleanse(ASTUtils.findOwnProperty(object4, "a")), identifier, + "object property a's value is identifier a"); }); test("getValue", (t) => {