From e691dad94c7be0a430803e7e69ec71ba53546d91 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Wed, 5 Oct 2022 14:57:37 +0300 Subject: [PATCH] [INTERNAL] jsdoc: Support destructuring of enums for defaultValue This change integrates the following Pull request from ui5-builder project: https://github.com/SAP/ui5-builder/pull/775 Change-Id: Id0cc3e903dbb3b0733add647ca9096d31b163051 --- lib/jsdoc/ui5/plugin.js | 347 +++++++++++++++++++++++++++++++++++----- 1 file changed, 308 insertions(+), 39 deletions(-) diff --git a/lib/jsdoc/ui5/plugin.js b/lib/jsdoc/ui5/plugin.js index 9a11b0619e4f..b762e130f37c 100644 --- a/lib/jsdoc/ui5/plugin.js +++ b/lib/jsdoc/ui5/plugin.js @@ -66,6 +66,7 @@ const fs = require('jsdoc/fs'); const path = require('jsdoc/path'); const logger = require('jsdoc/util/logger'); const pluginConfig = (env.conf && env.conf.templates && env.conf.templates.ui5) || env.opts.sapui5 || {}; +const escope = require("escope"); /* ---- logging ---- */ @@ -115,6 +116,9 @@ let docletUid = 0; let currentProgram; +// Scope Manager +let scopeManager; + /** * Information about the current module. * @@ -255,25 +259,33 @@ function analyzeModuleDefinition(node) { currentModule.dependencies = convertValue(args[arg], "string[]"); arg++; } - if ( arg < args.length && + 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].type === Syntax.ObjectPattern) - ? currentModule.factory.params[i].properties[0].value.name // ObjectPattern means destructuring of the parameter - : currentModule.factory.params[i].name; // simple Identifier + let names = []; - const module = resolveModuleName(currentModule.module, currentModule.dependencies[i]); - debug(` import ${name} from '${module}'`); - currentModule.localNames[name] = { - module: module - // no (or empty) path - }; + if ( [Syntax.ObjectPattern, Syntax.ArrayPattern].includes(currentModule.factory.params[i].type) ) { // ObjectPattern/ArrayPattern means destructuring of the parameter of a function + names = resolveObjectPatternChain(currentModule.factory.params[i], null, []); + + } else if (currentModule.factory.params[i].type === Syntax.Identifier) { // simple Identifier + names = [{original: currentModule.factory.params[i].name}]; + } + + names.forEach(name => { + const module = resolveModuleName(currentModule.module, currentModule.dependencies[i]); + debug(` import ${name.renamed || name.original} from '${module}'`); + + currentModule.localNames[name.renamed || name.original] = { + module: module, + ...(name.path ? {path: name.path} : {}) + }; + }); } } if ( currentModule.factory ) { @@ -460,7 +472,7 @@ function createPropertyMap(node, defaultKey) { /** * Resolves potential wrapper expressions like: ChainExpression, AwaitExpression, etc. - * @param {Node} node + * @param {Node} node * @returns {Node} the resolved node */ function resolvePotentialWrapperExpression(node) { @@ -484,9 +496,9 @@ function resolvePotentialWrapperExpression(node) { /** * Strips the ChainExpression wrapper if such - * - * @param {Node} rootNode - * @param {String} path + * + * @param {Node} rootNode + * @param {String} path * @returns {Node} */ function stripChainWrappers(rootNode, path) { @@ -516,8 +528,8 @@ function isTemplateLiteralWithoutExpression(node) { /** * Checks whether a node is Literal or TemplateLiteral without an expression - * - * @param {Node} node + * + * @param {Node} node * @returns {String} */ function isStringLiteral(node) { @@ -564,8 +576,8 @@ function isArrowFuncExpression(node) { /** * Checks whether the node is of a "returning" type - * - * @param {Node} node + * + * @param {Node} node * @returns {Boolean} */ function isReturningNode(node) { @@ -643,6 +655,254 @@ function isPotentialEnum(node) { return node.properties.every((prop) => isCompileTimeConstant(prop.value)); } +// ---- ES6+ Destructuring --------------------------------------------------------- + +/** + * Resolves (nested) Object/ArrayPattern nodes and builds a "path" + * + * @param {Node} valueNode + * @param {Node} keyNode + * @param {Array} keyChain + * @returns Array>> + */ +function resolveObjectPatternChain (valueNode, keyNode, keyChain) { + let chainSequence = []; + + if (valueNode && valueNode.type === Syntax.ObjectPattern) { + for (let i = 0; i < valueNode.properties.length; i++) { + chainSequence = chainSequence.concat( + resolveObjectPatternChain( + valueNode.properties[i].value, + valueNode.properties[i].key, + [...keyChain, valueNode.properties[i].key.name] ) + ); + } + } else if (valueNode && valueNode.type === Syntax.ArrayPattern) { + for (let i = 0; i < valueNode.elements.length; i++) { + chainSequence = chainSequence.concat( + resolveObjectPatternChain( + valueNode.elements[i], + valueNode.elements[i], + [...keyChain, String(i)] + ) + ); + } + } else { + + let result = { original: keyNode.name, path: keyChain.join(".") }; + + if (keyNode.name !== valueNode.name) { + // Renaming + result.renamed = valueNode.name; + } + + chainSequence.push(result); + } + + return chainSequence; +} + +/** + * Tries to resolve an ENUM, regardless where it is defined and being destructured. + * + * @param {Node} node + * @param {String} type + * @returns {Object} + */ +function resolvePotentialEnum(node, type) { + let value = resolveFullyQuantifiedName(node); + + if ( value.startsWith(type + ".") ) { + // starts with fully qualified enum name -> cut off name + value = value.slice(type.length + 1); + return { + value: value, + raw: value + }; + } +} + +/** + * Returns the {Node} of the destructured argument of a (arrow) function. + * + * @param {Definition|ParameterDefinition} varDefinition + * @returns {Node} + */ +function getFuncArgumentDestructNode(varDefinition) { + if ( + [ + Syntax.ArrowFunctionExpression, + Syntax.FunctionDeclaration, + Syntax.FunctionExpression, + ].includes(varDefinition.node.type) + ) { + return varDefinition.node.params[varDefinition.index]; + } + + // return undefined; +} + +/** + * Checks whether a variable has been destructured. + * + * @param {Variable} variable + * @returns + */ +function isVarDestructuring(variable) { + const defNode = + variable && + variable.defs && + ( getFuncArgumentDestructNode(variable.defs[0]) // (arrow) function argument + || variable.defs[0].node.id ); // variable definition + + return defNode && [Syntax.ObjectPattern, Syntax.ArrayPattern].includes( defNode.type ); +} + +/** + * Checks whether a var has been renamed while destructuring i.e. {A: b} = SomeObject + * + * @param {Variable} variable + * @returns {Object} + */ +function checkVarRenaming(variable) { + // variable.defs[0].node.id.type === Syntax.ObjectPattern -> Renaming + // variable.defs[0].node.id.properties[0].key.name === variable.name; // Original + // variable.defs[0].node.id.properties[0].value.name === variable.name; // Renamed + + // If variable.defs (Variable definitions within the source code) are more 1, then we'd not be able to + // determine which defintion to use. For example: + // function doSomething({a}, b) { + // console.log(a); + // var { c : a } = { b }; + // console.log(a); + // } + // doSomething({a:42}, 5); + // + // So, we'd not able to analyze which "a" to which console.log to map + if ( + !variable + || !variable.defs + || variable.defs.length !== 1 + ) { + return null; + } + + const varDefinition = variable.defs[0]; + const defNode = getFuncArgumentDestructNode(varDefinition) // (arrow) function argument + || varDefinition.node.id; // variable definition + + return resolveObjectPatternChain(defNode, null, []).find( + ({ original, renamed }) => + renamed === variable.name || original === variable.name + ); +} + +/** + * Builds the fully quantified name when there's a destructuring of a variable + * + * @param {Node} node + * @returns {string} + */ +function resolveFullyQuantifiedName(node) { + // The missing part is on the left side. The right side is clear. + // We would eiter ways resolve to the same leftmost token. + let leftMostName = getLeftmostName(node); + let originalName = getObjectName(node) || ""; + const currentScope = getEnclosingVariableScope(node); + + if (!currentScope) { + return ""; + } + + while (leftMostName) { + const curVar = currentScope.set.get(leftMostName); + + if (!curVar) { + break; + } + + if ( !isVarDestructuring(curVar) ) { + // Not a destructuring + return getResolvedName(originalName, leftMostName); + } + + const potentialRenaming = checkVarRenaming(curVar); + if (potentialRenaming) { + let renamedChunks = originalName.split("."); + renamedChunks = getResolvedName( originalName, renamedChunks[0] ).split("."); + + // when getResolvedName() was not able to resolve the renaming of a variable + // with currentModule.localNames[].path i.e. in "chained" destructuring, where + // the variable is not within localNames registry + if ( potentialRenaming.renamed === renamedChunks[0] ) { // Checks for direct renaming + renamedChunks[0] = potentialRenaming.original; + } + // Considers the 'path' if it differs from the original name i.e. there's some namespace before it + if ( potentialRenaming.original === renamedChunks[0] && potentialRenaming.path !== potentialRenaming.original ) { + renamedChunks[0] = potentialRenaming.path; + } + + originalName = renamedChunks.join("."); + } + + const writeExpr = curVar.references.filter((ref) => ref.writeExpr); + + // The same case as variable.defs- we're not able to determine the correct chain in case of multiple writeExpr + if (writeExpr.length !== 1) { + // writeExpr.length === 0 means an function argument and then we need + // just to return the already build originalName + return writeExpr.length === 0 ? originalName : ""; + } + + const writeExprNode = writeExpr[0].writeExpr; + + if ( writeExprNode.type === Syntax.MemberExpression && !writeExprNode.computed && writeExprNode.object.type === Syntax.Identifier ) { + leftMostName = writeExprNode.object.name; + + } else if (writeExprNode.type === Syntax.MemberExpression && writeExprNode.object.type === Syntax.MemberExpression) { // Standalone variable without leading dot notation namespace + leftMostName = getResolvedObjectName(writeExprNode); + + } else if (writeExprNode.type === Syntax.Identifier) { + leftMostName = writeExprNode.name; + + } else { + leftMostName = ""; + } + + if (leftMostName) { + originalName = leftMostName + "." + originalName; + } + } + + return originalName; +} + +/** + * Gets enclosing scope + * + * @param {Node} node + * @returns {Scope} + */ + function getEnclosingVariableScope (node) { + // Get to the nearest upper scope + let nearestScopeableNode = node; + let nearestScope = scopeManager.acquire(nearestScopeableNode); + while ( + !nearestScope && + nearestScopeableNode && + nearestScopeableNode.parent + ) { + nearestScopeableNode = nearestScopeableNode.parent; + nearestScope = scopeManager.acquire(nearestScopeableNode); + } + + // FunctionExpression with a name hold the value of the name in its scope. + // So, we need to unwrap to get to the real scope with var definitions + return nearestScope.functionExpressionScope + ? nearestScope.childScopes[0] + : nearestScope; +} + function isCompileTimeConstant(node) { return node && node.type === Syntax.Literal; } @@ -675,6 +935,11 @@ function getLeftmostName(node) { function getResolvedObjectName(node) { const name = getObjectName(node); const _import = getLeftmostName(node); + + return getResolvedName(name, _import); +} + +function getResolvedName(name, _import) { const local = _import && currentModule.localNames[_import]; if ( name && local && (local.class || local.module) ) { let resolvedName; @@ -734,18 +999,12 @@ function convertValueWithRaw(node, type, propertyName) { } else if ( isMemberExpression(node) && type ) { // enum value (a.b.c) - value = getResolvedObjectName(node); - if ( value.indexOf(type + ".") === 0 ) { - // starts with fully qualified enum name -> cut off name - value = value.slice(type.length + 1); - return { - value: value, - raw: value - }; -// } else if ( value.indexOf(type.split(".").slice(-1)[0] + ".") === 0 ) { -// // unqualified name might be a local name (just a guess - would need static code analysis for proper solution) -// return value.slice(type.split(".").slice(-1)[0].length + 1); + const potentialEnum = resolvePotentialEnum(node, type); + + if ( potentialEnum ) { + return potentialEnum; } else { + value = getResolvedObjectName(node); warning(`did not understand default value '${value}'${propertyName ? " of property '" + propertyName + "'" : ""}, falling back to source`); let raw = value; if ( currentSource && node.range ) { @@ -773,6 +1032,15 @@ function convertValueWithRaw(node, type, propertyName) { raw: local.raw }; } + + // This could be an ENUM which has been destructured up to the enum value part. In that case the node.type === Syntax.Identifier + // For example, the `Solid` const in the following snippet: + // sap.ui.define(["sap/m/library"], ( { BackgroundDesign } ) => { + // const { Solid } = BackgroundDesign; + const potentialEnum = resolvePotentialEnum(node, type); + if ( potentialEnum ) { + return potentialEnum; + } } else if ( node.type === Syntax.ArrayExpression ) { if ( node.elements.length === 0 ) { @@ -1232,16 +1500,16 @@ function determineValueRange(expression, varname, inverse) { && expression.left.type === Syntax.BinaryExpression && expression.right.type === Syntax.BinaryExpression ) { - if ( expression.operator === "&&" - && determineValueRangeBorder(range, expression.left, varname, inverse) + 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.left, varname, inverse) || determineValueRangeBorder(range, expression.right, varname, inverse) )) { return range; } - + } else if ( expression.type === Syntax.BinaryExpression && determineValueRangeBorder(range, expression, varname, inverse) ) { return range; @@ -2698,6 +2966,7 @@ exports.astNodeVisitor = { if ( node.type === Syntax.Program ) { currentProgram = node; + scopeManager = escope.analyze(currentProgram); } function processExtendCall(extendCall, comment, commentAlreadyProcessed) { @@ -2859,7 +3128,7 @@ exports.astNodeVisitor = { * to retain the full record type structure. * * JSDoc is copyright (c) 2011-present Michael Mathews micmath@gmail.com and the contributors to JSDoc. - */ + */ function getTypeStrings(parsedType, isOutermostType) { let applications; let typeString; @@ -2964,11 +3233,11 @@ exports.astNodeVisitor = { // #### BEGIN: MODIFIED BY SAP if ( tagInfo && (/function/.test(tagInfo.typeExpression) || /\{.*\}/.test(tagInfo.typeExpression)) && tagInfo.parsedType ) { // #### END: MODIFIED BY SAP - // console.info("old typeExpression", tagInfo.typeExpression); - // console.info("old parse tree", tagInfo.parsedType); - // console.info("old parse result", tagInfo.type); + // console.info("old typeExpression", tagInfo.typeExpression); + // console.info("old parse tree", tagInfo.parsedType); + // console.info("old parse result", tagInfo.type); tagInfo.type = getTypeStrings(tagInfo.parsedType); - // console.info("new parse result", tagInfo.type); + // console.info("new parse result", tagInfo.type); } return tagInfo; }