From 187a6a35000145d70bf41e0d8b724e5ea8d8dc78 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Fri, 9 Sep 2022 08:45:52 +0300 Subject: [PATCH] [FEATURE] jsdoc: Improve support for ES6+ syntax (#785) JIRA: CPOUI5FOUNDATION-374 ### Enabled features: * ArrowFunctions + ArrowFunctionExpression.expression * Avoid "computed" Identifier to be used at it might lead to hidden bugs * Optional Chaining for MemberExpression & CallExpression * Literals for Syntax.OptionalMemberExpression/OptionalCallExpression * Enhance LogicalExpression to support "||" and "??" oprators * Cover ChainExpression * Wrapper Expressions generic approach * Cover possible returning statements in JS * Support alternate definitions of modules * ES6 Class definition export * Support of TemplateLiterals - only withput expression - treated like normal strings --- lib/processors/jsdoc/lib/ui5/plugin.js | 392 +++++++++++++----- .../resources/library/j/dependency-es6-1.js | 31 ++ .../resources/library/j/dependency-es6-2.js | 46 ++ .../resources/library/j/dependency-es6-3.js | 12 + .../dest/resources/library/j/some.js | 26 +- .../library/j/designtime/api.json | 2 +- .../main/src/library/j/dependency-es6-1.js | 31 ++ .../main/src/library/j/dependency-es6-2.js | 46 ++ .../main/src/library/j/dependency-es6-3.js | 12 + .../library.j/main/src/library/j/some.js | 26 +- 10 files changed, 486 insertions(+), 138 deletions(-) create mode 100644 test/expected/build/library.j/dest/resources/library/j/dependency-es6-1.js create mode 100644 test/expected/build/library.j/dest/resources/library/j/dependency-es6-2.js create mode 100644 test/expected/build/library.j/dest/resources/library/j/dependency-es6-3.js create mode 100644 test/fixtures/library.j/main/src/library/j/dependency-es6-1.js create mode 100644 test/fixtures/library.j/main/src/library/j/dependency-es6-2.js create mode 100644 test/fixtures/library.j/main/src/library/j/dependency-es6-3.js diff --git a/lib/processors/jsdoc/lib/ui5/plugin.js b/lib/processors/jsdoc/lib/ui5/plugin.js index 5cbe12878..951d89e6b 100644 --- a/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/lib/processors/jsdoc/lib/ui5/plugin.js @@ -53,7 +53,14 @@ */ /* imports */ -const Syntax = require('jsdoc/src/syntax').Syntax; +const Syntax = { + // Those are currently not in the syntax.js file. + ChainExpression: "ChainExpression", + OptionalMemberExpression: "OptionalMemberExpression", + OptionalCallExpression: "OptionalCallExpression", + + ...require("jsdoc/src/syntax").Syntax, +}; const Doclet = require('jsdoc/doclet').Doclet; const fs = require('jsdoc/fs'); const path = require('jsdoc/path'); @@ -234,9 +241,8 @@ function resolveModuleName(base, name) { function analyzeModuleDefinition(node) { const args = node.arguments; let arg = 0; - if ( arg < args.length - && args[arg].type === Syntax.Literal && typeof args[arg].value === 'string' ) { - currentModule.name = args[arg].value; + if ( arg < args.length && isStringLiteral(args[arg]) ) { + currentModule.name = convertValue(args[arg]); warning(`module explicitly defined a module name '${currentModule.name}'`); const resourceModuleName = getModuleName(currentModule.resource); if (currentModule.name !== resourceModuleName) { @@ -249,13 +255,19 @@ function analyzeModuleDefinition(node) { currentModule.dependencies = convertValue(args[arg], "string[]"); arg++; } - if ( arg < args.length && args[arg].type === Syntax.FunctionExpression ) { + if ( arg < args.length && + [Syntax.FunctionExpression, Syntax.ArrowFunctionExpression].includes(args[arg].type)) { + currentModule.factory = args[arg]; arg++; } if ( currentModule.dependencies && currentModule.factory ) { for ( let i = 0; i < currentModule.dependencies.length && i < currentModule.factory.params.length; i++ ) { - const name = currentModule.factory.params[i].name; + const name = + (currentModule.factory.params[i].type === Syntax.ObjectPattern) + ? currentModule.factory.params[i].properties[0].value.name // ObjectPattern means destructuring of the parameter + : currentModule.factory.params[i].name; // simple Identifier + const module = resolveModuleName(currentModule.module, currentModule.dependencies[i]); debug(` import ${name} from '${module}'`); currentModule.localNames[name] = { @@ -265,7 +277,7 @@ function analyzeModuleDefinition(node) { } } if ( currentModule.factory ) { - collectShortcuts(currentModule.factory.body); + collectShortcuts(currentModule.factory); } } @@ -276,16 +288,19 @@ function analyzeModuleDefinition(node) { * * @param {ASTNode} body AST node of a function body that shall be searched for shortcuts */ -function collectShortcuts(body) { +function collectShortcuts(factory) { + const body = factory.body; function checkAssignment(name, valueNode) { + valueNode = resolvePotentialWrapperExpression(valueNode); + if ( valueNode.type === Syntax.Literal ) { currentModule.localNames[name] = { value: valueNode.value, raw: valueNode.raw }; debug("compile time constant found ", name, valueNode.value); - } else if ( valueNode.type === Syntax.MemberExpression ) { + } else if ( isMemberExpression(valueNode) ) { const _import = getLeftmostName(valueNode); const local = _import && currentModule.localNames[_import]; const objectName = getObjectName(valueNode); @@ -297,11 +312,9 @@ function collectShortcuts(body) { debug(` found local shortcut: ${name} ${currentModule.localNames[name]}`); } } else if ( isRequireSyncCall(valueNode) || isProbingRequireCall(valueNode) ) { - if ( valueNode.arguments[0] - && valueNode.arguments[0].type === Syntax.Literal - && typeof valueNode.arguments[0].value === 'string' ) { + if ( valueNode.arguments[0] && isStringLiteral(valueNode.arguments[0]) ) { currentModule.localNames[name] = { - module: valueNode.arguments[0].value + module: convertValue(valueNode.arguments[0]) // no (or empty) path }; debug(` found local import: ${name} = ${valueNode.callee.property.name}('${valueNode.arguments[0].value}')`); @@ -315,8 +328,8 @@ function collectShortcuts(body) { } } - if ( body.type === Syntax.BlockStatement ) { - body.body.forEach(function ( stmt ) { + if ( body.type === Syntax.BlockStatement || isArrowFuncExpression(factory) ) { + const itemsResolver = function ( stmt ) { // console.log(stmt); if ( stmt.type === Syntax.FunctionDeclaration ) { if ( stmt.id && stmt.id.type === Syntax.Identifier && stmt.loc && stmt.loc.start ) { @@ -336,15 +349,28 @@ function collectShortcuts(body) { && stmt.expression.type === Syntax.AssignmentExpression && stmt.expression.left.type === Syntax.Identifier ) { checkAssignment(stmt.expression.left.name, stmt.expression.right); - } else if ( stmt.type === Syntax.ReturnStatement ) { - if ( stmt.argument && stmt.argument.type === Syntax.Identifier ) { - currentModule.defaultExport = stmt.argument.name; - } else if ( stmt.argument && isExtendCall(stmt.argument) ) { - debug(` found default export class definition: return .extend('${stmt.argument.arguments[0].value}', ...)`); - currentModule.defaultExportClass = stmt.argument.arguments[0].value; + } else if ( isReturningNode(stmt) ) { + const stmtArgument = isArrowFuncExpression(stmt) + ? stmt.body + : resolvePotentialWrapperExpression(stmt).argument; + + if ( stmtArgument && stmtArgument.type === Syntax.Identifier ) { + currentModule.defaultExport = stmtArgument.name; + } else if ( stmtArgument && stmtArgument.type === Syntax.ClassExpression && stmtArgument.id && stmtArgument.id.type === Syntax.Identifier ) { + debug(` found default export class definition: return class '${stmtArgument.id.name}'`); + currentModule.defaultExportClass = stmtArgument.id.name; + } else if ( stmtArgument && isExtendCall(stmtArgument) ) { + debug(` found default export class definition: return .extend('${stmtArgument.arguments[0].value}', ...)`); + currentModule.defaultExportClass = convertValue(stmtArgument.arguments[0]); } } - }); + }; + + if (isArrowFuncExpression(factory)) { + itemsResolver(factory); + } else { + body.body.forEach(itemsResolver); + } } if ( currentModule.defaultExport && currentModule.localNames[currentModule.defaultExport] ) { @@ -369,12 +395,12 @@ function guessSingularName(sPluralName) { } function getPropertyKey(prop) { - if ( prop.key.type === Syntax.Identifier ) { + if ( prop.type === Syntax.SpreadElement ) { + return; + } else if ( prop.key.type === Syntax.Identifier && prop.computed !== true ) { return prop.key.name; } else if ( prop.key.type === Syntax.Literal ) { return String(prop.key.value); - } else { - return prop.key.toSource(); } } @@ -432,80 +458,180 @@ function createPropertyMap(node, defaultKey) { return result; } +/** + * Resolves potential wrapper expressions like: ChainExpression, AwaitExpression, etc. + * @param {Node} node + * @returns {Node} the resolved node + */ +function resolvePotentialWrapperExpression(node) { + switch (node && node.type) { + case Syntax.ChainExpression: + return node.expression; + + case Syntax.AwaitExpression: + return node.argument; + + case Syntax.ExpressionStatement: + if ( node.expression.type === Syntax.YieldExpression ) { + return node.expression.argument.type === Syntax.UpdateExpression + ? node.expression.argument + : node.expression; + } + default: + return node; + } +} + +/** + * Strips the ChainExpression wrapper if such + * + * @param {Node} rootNode + * @param {String} path + * @returns {Node} + */ +function stripChainWrappers(rootNode, path) { + const strip = (node) => + node && node.type === Syntax.ChainExpression ? node.expression : node; + + let curNode = strip(rootNode); + let chunks = path && path.split("."); + let name; + + while (chunks && chunks.length) { + name = chunks.shift(); + curNode = curNode && strip(curNode[name]); + } + + return curNode; +} + +function isTemplateLiteralWithoutExpression(node) { + return ( + node?.type === Syntax.TemplateLiteral && + node?.expressions?.length === 0 && + node?.quasis?.length === 1 + ); +} + +/** + * Checks whether a node is Literal or TemplateLiteral without an expression + * + * @param {Node} node + * @returns {String} + */ +function isStringLiteral(node) { + return ( + (node && node.type === Syntax.Literal && typeof node.value === "string") + || isTemplateLiteralWithoutExpression(node) + ); +} + +function isMemberExpression(node) { + return node && [Syntax.MemberExpression, Syntax.OptionalMemberExpression].includes(node.type); +} + +function isCaleeMemberExpression(node) { + return ( + node && + [Syntax.CallExpression, Syntax.OptionalCallExpression].includes(node.type) && + isMemberExpression(node.callee) + ); +} + function isExtendCall(node) { + node = stripChainWrappers(node); return ( node - && node.type === Syntax.CallExpression - && node.callee.type === Syntax.MemberExpression - && node.callee.property.type === Syntax.Identifier - && node.callee.property.name === 'extend' + && isCaleeMemberExpression(node) + && stripChainWrappers(node, "callee.property").type === Syntax.Identifier + && stripChainWrappers(node, "callee.property").name === 'extend' && node.arguments.length >= 2 - && node.arguments[0].type === Syntax.Literal - && typeof node.arguments[0].value === "string" + && isStringLiteral(node.arguments[0]) && node.arguments[1].type === Syntax.ObjectExpression ); } +function isArrowFuncExpression(node) { + return ( + node && + node.type === Syntax.ArrowFunctionExpression && + node.expression === true + ); +} + +/** + * Checks whether the node is of a "returning" type + * + * @param {Node} node + * @returns {Boolean} + */ +function isReturningNode(node) { + return (node && node.type === Syntax.ReturnStatement) + || (node && node.type === Syntax.ExpressionStatement && node.expression.type === Syntax.YieldExpression) + || isArrowFuncExpression(node); +} + function isSapUiDefineCall(node) { return ( - node - && node.type === Syntax.CallExpression - && node.callee.type === Syntax.MemberExpression - && node.callee.object.type === Syntax.MemberExpression - && node.callee.object.object.type === Syntax.Identifier - && node.callee.object.object.name === 'sap' - && node.callee.object.property.type === Syntax.Identifier - && node.callee.object.property.name === 'ui' - && node.callee.property.type === Syntax.Identifier - && node.callee.property.name === 'define' + stripChainWrappers(node) + && isCaleeMemberExpression(stripChainWrappers(node)) + && isMemberExpression(stripChainWrappers(node, "callee.object")) + && stripChainWrappers(node, "callee.property").type === Syntax.Identifier + && stripChainWrappers(node, "callee.property").name === 'define' + && stripChainWrappers(node, "callee.object.object").type === Syntax.Identifier + && stripChainWrappers(node, "callee.object.object").name === 'sap' + && stripChainWrappers(node, "callee.object.property").type === Syntax.Identifier + && stripChainWrappers(node, "callee.object.property").name === 'ui' ); } function isCreateDataTypeCall(node) { + node = stripChainWrappers(node); + return ( node - && node.type === Syntax.CallExpression - && node.callee.type === Syntax.MemberExpression - && getResolvedObjectName(node.callee.object) === "sap.ui.base.DataType" - && node.callee.property.type === Syntax.Identifier - && node.callee.property.name === 'createType' + && isCaleeMemberExpression(node) + && getResolvedObjectName(stripChainWrappers(node, "callee.object")) === "sap.ui.base.DataType" + && stripChainWrappers(node, "callee.property").type === Syntax.Identifier + && stripChainWrappers(node, "callee.property").name === 'createType' ); } function isRequireSyncCall(node) { + node = stripChainWrappers(node); + return ( node - && node.type === Syntax.CallExpression - && node.callee.type === Syntax.MemberExpression - && node.callee.object.type === Syntax.MemberExpression - && node.callee.object.object.type === Syntax.Identifier - && node.callee.object.object.name === 'sap' - && node.callee.object.property.type === Syntax.Identifier - && node.callee.object.property.name === 'ui' - && node.callee.property.type === Syntax.Identifier - && node.callee.property.name === 'requireSync' + && isCaleeMemberExpression(node) + && isMemberExpression( stripChainWrappers(node, "callee.object") ) + && stripChainWrappers(node, "callee.object.object").type === Syntax.Identifier + && stripChainWrappers(node, "callee.object.object").name === 'sap' + && stripChainWrappers(node, "callee.object.property").type === Syntax.Identifier + && stripChainWrappers(node, "callee.object.property").name === 'ui' + && stripChainWrappers(node, "callee.property").type === Syntax.Identifier + && stripChainWrappers(node, "callee.property").name === 'requireSync' ); } function isProbingRequireCall(node) { + node = stripChainWrappers(node); + return ( node - && node.type === Syntax.CallExpression - && node.callee.type === Syntax.MemberExpression - && node.callee.object.type === Syntax.MemberExpression - && node.callee.object.object.type === Syntax.Identifier - && node.callee.object.object.name === 'sap' - && node.callee.object.property.type === Syntax.Identifier - && node.callee.object.property.name === 'ui' - && node.callee.property.type === Syntax.Identifier - && node.callee.property.name === 'require' + && isCaleeMemberExpression(node) + && isMemberExpression( stripChainWrappers(node, "callee.object") ) + && stripChainWrappers(node, "callee.object.object").type === Syntax.Identifier + && stripChainWrappers(node, "callee.object.object").name === 'sap' + && stripChainWrappers(node, "callee.object.property").type === Syntax.Identifier + && stripChainWrappers(node, "callee.object.property").name === 'ui' + && stripChainWrappers(node, "callee.property").type === Syntax.Identifier + && stripChainWrappers(node, "callee.property").name === 'require' && node.arguments.length === 1 - && node.arguments[0].type === Syntax.Literal - && typeof node.arguments[0].value === 'string' // TODO generalize to statically analyzable constants + && isStringLiteral(node.arguments[0]) ); } @@ -521,7 +647,7 @@ function isCompileTimeConstant(node) { } function getObjectName(node) { - if ( node.type === Syntax.MemberExpression && !node.computed && node.property.type === Syntax.Identifier ) { + if ( isMemberExpression(node) && !node.computed && node.property.type === Syntax.Identifier ) { const prefix = getObjectName(node.object); return prefix ? prefix + "." + node.property.name : null; } else if ( node.type === Syntax.Identifier ) { @@ -536,7 +662,7 @@ function getObjectName(node) { * returns the leftmost identifier a */ function getLeftmostName(node) { - while ( node.type === Syntax.MemberExpression ) { + while ( isMemberExpression(node) ) { node = node.object; } if ( node.type === Syntax.Identifier ) { @@ -604,7 +730,7 @@ function convertValueWithRaw(node, type, propertyName) { raw: node.operator + node.argument.raw }; - } else if ( node.type === Syntax.MemberExpression && type ) { + } else if ( isMemberExpression(node) && type ) { // enum value (a.b.c) value = getResolvedObjectName(node); @@ -674,6 +800,11 @@ function convertValueWithRaw(node, type, propertyName) { }; } + } else if ( isTemplateLiteralWithoutExpression(node) ) { + return { + value: node?.quasis?.[0]?.value?.cooked, + raw: node?.quasis?.[0]?.value?.raw, + }; } value = "...see text or source"; @@ -697,10 +828,10 @@ function convertStringArray(node) { } const result = []; for ( let i = 0; i < node.elements.length; i++ ) { - if ( node.elements[i].type !== Syntax.Literal || typeof node.elements[i].value !== 'string' ) { + if ( !isStringLiteral(node.elements[i]) ) { throw new Error("not a string literal"); } - result.push(node.elements[i].value); + result.push( convertValue(node.elements[i]) ); } // console.log(result); return result; @@ -736,7 +867,7 @@ function collectClassInfo(extendCall, classDoclet) { if ( classDoclet && classDoclet.augments && classDoclet.augments.length === 1 ) { baseType = classDoclet.augments[0]; } - if ( extendCall.callee.type === Syntax.MemberExpression ) { + if ( isMemberExpression(extendCall.callee) ) { const baseCandidate = getResolvedObjectName(extendCall.callee.object); if ( baseCandidate && baseType == null ) { baseType = baseCandidate; @@ -746,7 +877,7 @@ function collectClassInfo(extendCall, classDoclet) { } const oClassInfo = { - name : extendCall.arguments[0].value, + name : convertValue(extendCall.arguments[0]), baseType : baseType, interfaces : [], doc : classDoclet && classDoclet.description, @@ -988,7 +1119,7 @@ function collectClassInfo(extendCall, classDoclet) { return oClassInfo; } -function collectDesigntimeInfo(dtNode) { +function collectDesigntimeInfo(dtNodeArgument) { function each(node, defaultKey, callback) { const map = node && createPropertyMap(node.value); @@ -1010,7 +1141,7 @@ function collectDesigntimeInfo(dtNode) { let oDesigntimeInfo; - const map = createPropertyMap(dtNode.argument); + const map = createPropertyMap(dtNodeArgument); if (map.annotations) { @@ -1095,12 +1226,19 @@ function determineValueRangeBorder(range, expression, varname, inverse) { function determineValueRange(expression, varname, inverse) { const range = {}; if ( expression.type === Syntax.LogicalExpression - && expression.operator === '&&' && expression.left.type === Syntax.BinaryExpression - && expression.right.type === Syntax.BinaryExpression - && determineValueRangeBorder(range, expression.left, varname, inverse) - && determineValueRangeBorder(range, expression.right, varname, inverse) ) { - return range; + && expression.right.type === Syntax.BinaryExpression ) { + + if ( expression.operator === "&&" + && determineValueRangeBorder(range, expression.left, varname, inverse) + && determineValueRangeBorder(range, expression.right, varname, inverse) ) { + return range; + } else if ( ["||", "??"].includes(expression.operator) + && ( determineValueRangeBorder(range, expression.left, varname, inverse) + || determineValueRangeBorder(range, expression.right, varname, inverse) )) { + return range; + } + } else if ( expression.type === Syntax.BinaryExpression && determineValueRangeBorder(range, expression, varname, inverse) ) { return range; @@ -1113,24 +1251,24 @@ function collectDataTypeInfo(extendCall, classDoclet) { let i = 0, name, def, base, pattern, range; - if ( i < args.length && args[i].type === Syntax.Literal && typeof args[i].value === 'string' ) { - name = args[i++].value; + if ( i < args.length && isStringLiteral(args[i]) ) { + name = convertValue(args[i++]); } if ( i < args.length && args[i].type === Syntax.ObjectExpression ) { def = createPropertyMap(args[i++]); } if ( i < args.length ) { - if ( args[i].type === Syntax.Literal && typeof args[i].value === 'string' ) { - base = args[i++].value; - } else if ( args[i].type === Syntax.CallExpression - && args[i].callee.type === Syntax.MemberExpression - && getResolvedObjectName(args[i].callee.object) === "sap.ui.base.DataType" - && args[i].callee.property.type === Syntax.Identifier - && args[i].callee.property.name === 'getType' - && args[i].arguments.length === 1 - && args[i].arguments[0].type === Syntax.Literal - && typeof args[i].arguments[0].value === 'string' ) { - base = args[i++].arguments[0].value; + const node = resolvePotentialWrapperExpression(args[i]); + + if ( isStringLiteral(args[i]) ) { + base = convertValue(args[i++]); + } else if ( isCaleeMemberExpression(node) + && getResolvedObjectName(node.callee.object) === "sap.ui.base.DataType" + && node.callee.property.type === Syntax.Identifier + && node.callee.property.name === 'getType' + && node.arguments.length === 1 + && isStringLiteral(node.arguments[0]) ) { + base = convertValue(args[i++].arguments[0]); } else { future(`could not identify base type of data type '${name}'`); } @@ -1138,25 +1276,31 @@ function collectDataTypeInfo(extendCall, classDoclet) { base = "any"; } + const isArrowExpression = isArrowFuncExpression(def && def.isValid && def.isValid.value); + if ( def && def.isValid - && def.isValid.value.type === Syntax.FunctionExpression + && [Syntax.FunctionExpression, Syntax.ArrowFunctionExpression].includes(def.isValid.value.type) && def.isValid.value.params.length === 1 && def.isValid.value.params[0].type === Syntax.Identifier - && def.isValid.value.body.body.length === 1 ) { + && (isArrowExpression || def.isValid.value.body.body.length === 1) ) { + const varname = def.isValid.value.params[0].name; - const stmt = def.isValid.value.body.body[0]; - if ( stmt.type === Syntax.ReturnStatement && stmt.argument ) { - if ( stmt.argument.type === Syntax.CallExpression - && stmt.argument.callee.type === Syntax.MemberExpression - && stmt.argument.callee.object.type === Syntax.Literal - && stmt.argument.callee.object.regex - && stmt.argument.callee.property.type === Syntax.Identifier - && stmt.argument.callee.property.name === 'test' ) { - pattern = stmt.argument.callee.object.regex.pattern; + const stmt = isArrowExpression ? def.isValid.value.body : def.isValid.value.body.body[0]; + + if ( isReturningNode(stmt) || isArrowExpression ) { + const stmtArgument = resolvePotentialWrapperExpression( + isArrowExpression ? stmt : stmt.argument + ); + if ( isCaleeMemberExpression(stmtArgument) + && stmtArgument.callee.object.type === Syntax.Literal + && stmtArgument.callee.object.regex + && stmtArgument.callee.property.type === Syntax.Identifier + && stmtArgument.callee.property.name === 'test' ) { + pattern = stmtArgument.callee.object.regex.pattern; // console.log(pattern); } else { - range = determineValueRange(stmt.argument, varname, false); + range = determineValueRange(stmtArgument, varname, false); } } else if ( stmt.type === Syntax.IfStatement && stmt.consequent.type === Syntax.BlockStatement @@ -2583,24 +2727,37 @@ exports.astNodeVisitor = { } } - if ( node.type === Syntax.ExpressionStatement ) { - if ( isSapUiDefineCall(node.expression) ) { - analyzeModuleDefinition(node.expression); - /* + if ( [Syntax.ExpressionStatement, Syntax.LogicalExpression].includes(node.type) ) { + let nodeToAnalyze; + if (isSapUiDefineCall(node.expression)) { + nodeToAnalyze = node.expression; + + /* } else if ( isJQuerySapDeclareCall(node.expression) && node.expression.arguments.length > 0 && node.expression.arguments[0].type === Syntax.Literal && typeof node.expression.arguments[0].value === "string" ) { warning(`module has explicit module name ${node.expression.arguments[0].value}`); */ + } else if (isSapUiDefineCall(node.left)) { + nodeToAnalyze = node.left; + } else if (isSapUiDefineCall(node.right)) { + nodeToAnalyze = node.right; } + nodeToAnalyze && analyzeModuleDefinition( + resolvePotentialWrapperExpression(nodeToAnalyze) + ); } - if (node.type === Syntax.ReturnStatement && node.argument && node.argument.type === Syntax.ObjectExpression && /\.designtime\.js$/.test(currentSourceName) ) { + const isArrowExpression = isArrowFuncExpression(node) && node.body.type === Syntax.ObjectExpression; + const nodeArgument = isArrowExpression + ? node.body + : resolvePotentialWrapperExpression(node).argument; + if (isArrowExpression || (isReturningNode(node) && nodeArgument && nodeArgument.type === Syntax.ObjectExpression) && /\.designtime\.js$/.test(currentSourceName) ) { // assume this node to return designtime metadata. Collect it and remember it by its module name - const oDesigntimeInfo = collectDesigntimeInfo(node); + const oDesigntimeInfo = collectDesigntimeInfo(nodeArgument); if ( oDesigntimeInfo ) { designtimeInfos[currentModule.module] = oDesigntimeInfo; info(`collected designtime info ${currentModule.module}`); @@ -2630,14 +2787,19 @@ exports.astNodeVisitor = { } }); - } else if ( node.type === Syntax.ReturnStatement && isExtendCall(node.argument) ) { + } else if ( isReturningNode(node) + && isExtendCall(resolvePotentialWrapperExpression(node).argument || node.body) ) { + + const nodeArgument = isArrowFuncExpression(node) + ? node.body + : resolvePotentialWrapperExpression(node).argument; // return Something.extend(...) - const className = node.argument.arguments[0].value; - const comment = getLeadingCommentNode(node, className) || getLeadingCommentNode(node.argument, className); + const className = convertValue(nodeArgument.arguments[0]); + const comment = getLeadingCommentNode(node, className) || getLeadingCommentNode(nodeArgument, className); // console.log(`ast node with comment ${comment}`); - processExtendCall(node.argument, comment, true); + processExtendCall(nodeArgument, comment, true); } else if ( node.type === Syntax.ExpressionStatement && node.expression.type === Syntax.AssignmentExpression ) { if ( isCreateDataTypeCall(node.expression.right) ) { diff --git a/test/expected/build/library.j/dest/resources/library/j/dependency-es6-1.js b/test/expected/build/library.j/dest/resources/library/j/dependency-es6-1.js new file mode 100644 index 000000000..1bec63ae1 --- /dev/null +++ b/test/expected/build/library.j/dest/resources/library/j/dependency-es6-1.js @@ -0,0 +1,31 @@ +/*! + * ${copyright} + */ + +/** + * Covers: + * - ArrowFunction + * - ChainExpression + * - ClassDeclaration + */ +(sap?.ui).define([`Bar`], (Bar) => { + /** + * @class + * My super documentation of this class + * + * @extends library.j.Bar + * + * @author SAP SE + * @version ${version} + * + * @public + * @alias library.j.Foo + */ + class Foo extends Bar { + make() { + sap.ui.require("conditional/module1"); + } + } + + return Foo; +}); diff --git a/test/expected/build/library.j/dest/resources/library/j/dependency-es6-2.js b/test/expected/build/library.j/dest/resources/library/j/dependency-es6-2.js new file mode 100644 index 000000000..541fe9b5f --- /dev/null +++ b/test/expected/build/library.j/dest/resources/library/j/dependency-es6-2.js @@ -0,0 +1,46 @@ +/*! + * ${copyright} + */ + +/** + * Covers: + * - ArrowFunctionExpression + */ +window.someRandomModule || + sap.ui.define( + ["./a"], + /** + * Constructor for a new library.j.aaa. + * + * @param {string} [sId] ID for the new control, generated automatically if no ID is given + * @param {object} [mSettings] Initial settings for the new control + * + * @class + * + * @author SAP SE + * @version ${version} + * + * @constructor + * @extends library.j.a + * @public + * @since 1.22 + * @alias library.j.aaa + * @ui5-metamodel This control will also be described in the UI5 (legacy) design time meta model. + */ + (a) => + a.extend(`library.j.aaa`, { + metadata: { + properties: { + /** + * MyProp property + * @since 1.46 + */ + MyProp: { + type: "boolean", + group: `Misc`, + defaultValue: false, + }, + }, + }, + }) + ); diff --git a/test/expected/build/library.j/dest/resources/library/j/dependency-es6-3.js b/test/expected/build/library.j/dest/resources/library/j/dependency-es6-3.js new file mode 100644 index 000000000..3f059af2c --- /dev/null +++ b/test/expected/build/library.j/dest/resources/library/j/dependency-es6-3.js @@ -0,0 +1,12 @@ +/*! + * ${copyright} + */ + +/** + * Covers: + * - Generators + * - YeldExpression + */ +sap.ui.define([], function* someGenerator(genVar) { + yield genVar++; +}); diff --git a/test/expected/build/library.j/dest/resources/library/j/some.js b/test/expected/build/library.j/dest/resources/library/j/some.js index 8fd81bb68..4d7f5afce 100644 --- a/test/expected/build/library.j/dest/resources/library/j/some.js +++ b/test/expected/build/library.j/dest/resources/library/j/some.js @@ -2,15 +2,19 @@ * ${copyright} */ -sap.ui.define([], - function() { - "use strict"; +sap.ui.define( + ["./dependency-es6-1"], + ["./dependency-es6-2"], + ["./dependency-es6-3"], + function (dep1, dep2, dep3) { + "use strict"; - /** - * @alias library.j - * @namespace - * @public - */ - var SomeFunction = function() {}; - -}, /* bExport= */ true); + /** + * @alias library.j + * @namespace + * @public + */ + var SomeFunction = function () {}; + }, + /* bExport= */ true +); diff --git a/test/expected/build/library.j/dest/test-resources/library/j/designtime/api.json b/test/expected/build/library.j/dest/test-resources/library/j/designtime/api.json index 6c7e52bc6..0bf0fd3a6 100644 --- a/test/expected/build/library.j/dest/test-resources/library/j/designtime/api.json +++ b/test/expected/build/library.j/dest/test-resources/library/j/designtime/api.json @@ -1 +1 @@ -{"$schema-ref":"http://schemas.sap.com/sapui5/designtime/api.json/1.0","version":"1.0.0","library":"library.j","symbols":[{"kind":"namespace","name":"library.j","basename":"j","resource":"library/j/some.js","module":"library/j/some","static":true,"visibility":"public"}]} \ No newline at end of file +{"$schema-ref":"http://schemas.sap.com/sapui5/designtime/api.json/1.0","version":"1.0.0","library":"library.j","symbols":[{"kind":"namespace","name":"library.j","basename":"j","resource":"library/j/some.js","module":"library/j/some","static":true,"visibility":"public"},{"kind":"class","name":"library.j.aaa","basename":"aaa","resource":"library/j/dependency-es6-2.js","module":"library/j/dependency-es6-2","export":"","static":true,"visibility":"public","since":"1.22","extends":"library.j.a","ui5-metamodel":true,"ui5-metadata":{"properties":[{"name":"MyProp","type":"boolean","defaultValue":false,"group":"undefined","visibility":"public","since":"1.46","description":"MyProp property","methods":["getMyProp","setMyProp"]}]},"constructor":{"visibility":"public","parameters":[{"name":"sId","type":"string","optional":true,"description":"ID for the new control, generated automatically if no ID is given"},{"name":"mSettings","type":"object","optional":true,"description":"Initial settings for the new control"}],"description":"Constructor for a new library.j.aaa."},"methods":[{"name":"extend","visibility":"public","static":true,"returnValue":{"type":"function","description":"Created class / constructor function"},"parameters":[{"name":"sClassName","type":"string","optional":false,"description":"Name of the class being created"},{"name":"oClassInfo","type":"object","optional":true,"description":"Object literal with information about the class"},{"name":"FNMetaImpl","type":"function","optional":true,"description":"Constructor function for the metadata object; if not given, it defaults to the metadata implementation used by this class"}],"description":"Creates a new subclass of class library.j.aaa with name sClassName and enriches it with the information contained in oClassInfo.\n\noClassInfo might contain the same kind of information as described in {@link library.j.a.extend}."},{"name":"getMetadata","visibility":"public","static":true,"returnValue":{"type":"sap.ui.base.Metadata","description":"Metadata object describing this class"},"description":"Returns a metadata object for class library.j.aaa."},{"name":"getMyProp","visibility":"public","since":"1.46","returnValue":{"type":"boolean","description":"Value of property MyProp"},"description":"Gets current value of property {@link #getMyProp MyProp}.\n\nMyProp property\n\nDefault value is false."},{"name":"setMyProp","visibility":"public","since":"1.46","returnValue":{"type":"this","description":"Reference to this in order to allow method chaining"},"parameters":[{"name":"bMyProp","type":"boolean","optional":true,"defaultValue":false,"description":"New value for property MyProp"}],"description":"Sets a new value for property {@link #getMyProp MyProp}.\n\nMyProp property\n\nWhen called with a value of null or undefined, the default value of the property will be restored.\n\nDefault value is false."}]},{"kind":"class","name":"library.j.Foo","basename":"Foo","resource":"library/j/dependency-es6-1.js","module":"library/j/dependency-es6-1","static":true,"visibility":"public","extends":"library.j.Bar","description":"My super documentation of this class","constructor":{"visibility":"public"}}]} \ No newline at end of file diff --git a/test/fixtures/library.j/main/src/library/j/dependency-es6-1.js b/test/fixtures/library.j/main/src/library/j/dependency-es6-1.js new file mode 100644 index 000000000..1bec63ae1 --- /dev/null +++ b/test/fixtures/library.j/main/src/library/j/dependency-es6-1.js @@ -0,0 +1,31 @@ +/*! + * ${copyright} + */ + +/** + * Covers: + * - ArrowFunction + * - ChainExpression + * - ClassDeclaration + */ +(sap?.ui).define([`Bar`], (Bar) => { + /** + * @class + * My super documentation of this class + * + * @extends library.j.Bar + * + * @author SAP SE + * @version ${version} + * + * @public + * @alias library.j.Foo + */ + class Foo extends Bar { + make() { + sap.ui.require("conditional/module1"); + } + } + + return Foo; +}); diff --git a/test/fixtures/library.j/main/src/library/j/dependency-es6-2.js b/test/fixtures/library.j/main/src/library/j/dependency-es6-2.js new file mode 100644 index 000000000..541fe9b5f --- /dev/null +++ b/test/fixtures/library.j/main/src/library/j/dependency-es6-2.js @@ -0,0 +1,46 @@ +/*! + * ${copyright} + */ + +/** + * Covers: + * - ArrowFunctionExpression + */ +window.someRandomModule || + sap.ui.define( + ["./a"], + /** + * Constructor for a new library.j.aaa. + * + * @param {string} [sId] ID for the new control, generated automatically if no ID is given + * @param {object} [mSettings] Initial settings for the new control + * + * @class + * + * @author SAP SE + * @version ${version} + * + * @constructor + * @extends library.j.a + * @public + * @since 1.22 + * @alias library.j.aaa + * @ui5-metamodel This control will also be described in the UI5 (legacy) design time meta model. + */ + (a) => + a.extend(`library.j.aaa`, { + metadata: { + properties: { + /** + * MyProp property + * @since 1.46 + */ + MyProp: { + type: "boolean", + group: `Misc`, + defaultValue: false, + }, + }, + }, + }) + ); diff --git a/test/fixtures/library.j/main/src/library/j/dependency-es6-3.js b/test/fixtures/library.j/main/src/library/j/dependency-es6-3.js new file mode 100644 index 000000000..3f059af2c --- /dev/null +++ b/test/fixtures/library.j/main/src/library/j/dependency-es6-3.js @@ -0,0 +1,12 @@ +/*! + * ${copyright} + */ + +/** + * Covers: + * - Generators + * - YeldExpression + */ +sap.ui.define([], function* someGenerator(genVar) { + yield genVar++; +}); diff --git a/test/fixtures/library.j/main/src/library/j/some.js b/test/fixtures/library.j/main/src/library/j/some.js index 8fd81bb68..4d7f5afce 100644 --- a/test/fixtures/library.j/main/src/library/j/some.js +++ b/test/fixtures/library.j/main/src/library/j/some.js @@ -2,15 +2,19 @@ * ${copyright} */ -sap.ui.define([], - function() { - "use strict"; +sap.ui.define( + ["./dependency-es6-1"], + ["./dependency-es6-2"], + ["./dependency-es6-3"], + function (dep1, dep2, dep3) { + "use strict"; - /** - * @alias library.j - * @namespace - * @public - */ - var SomeFunction = function() {}; - -}, /* bExport= */ true); + /** + * @alias library.j + * @namespace + * @public + */ + var SomeFunction = function () {}; + }, + /* bExport= */ true +);