UNPKG

@openui5/sap.ui.demokit

Version:

OpenUI5 UI Library sap.ui.demokit

707 lines (582 loc) 23.1 kB
/*! * OpenUI5 * (c) Copyright 2009-2021 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides implementation of sap.ui.demokit.util.jsanalyzer.EntityParser sap.ui.define(['jquery.sap.global', 'sap/ui/base/ManagedObjectMetadata', './ASTUtils', './Doclet', 'sap/ui/demokit/js/esprima'], function (jQuery, MOMetadata, ASTUtils, Doclet, esprima_) { "use strict"; /*global esprima */ var Syntax = esprima.Syntax; /* ---- private functions ---- */ /** * Name of the package in which the currently analyzed entity resides. * * Used to resolve relative dependencies in sap.ui.define calls. * * @type {string} * @private */ var currentPackage; /** * List of collected info objects. A JS file might contain multiple class and/or type definitions. * * @type {object[]} * @private */ var aInfos; /** * Object with all the parsed public functions * type {Object} */ var oPublicFunctions; /** * Name of the current control * type {string} */ var sControlName; /** * Cumulated scope information for the currently analyzed module. * * This is not the same as the Javascript scope of any of the functions in the module but it is a projection * of all scopes. It is only maintained to properly recognize sa.ui.base.DataType and some other core classes * with a specific meaning for the class / type analysis (e.g. jQuery). * * Keys in the map are names of local variables, values are their corresponding global name (if known). * * For a full fledged scope analysis, either the StaticAnalyzer needs to be migrated or an opensource * component like 'escope' could be integrated. * * @type {Map<string,string>} * @private */ var scope; // TODO implement scope properly using escope // some shortcuts var createPropertyMap = ASTUtils.createPropertyMap; var unlend = ASTUtils.unlend; var guessSingularName = MOMetadata._guessSingularName; var getLeadingDoclet = Doclet.get; var error = jQuery.sap.log.error; var warning = jQuery.sap.log.warning; var verbose = jQuery.sap.log.debug; function isExtendCall(node) { return ( node && node.type === Syntax.CallExpression && node.callee.type === Syntax.MemberExpression && node.callee.property.type === Syntax.Identifier && node.callee.property.name === 'extend' && node.arguments.length >= 2 && node.arguments[0].type === Syntax.Literal && typeof node.arguments[0].value === "string" && unlend(node.arguments[1]).type === Syntax.ObjectExpression ); } function isSapUiDefineCall(node) { return ( node && node.type === Syntax.CallExpression && node.callee.type === Syntax.MemberExpression && /* TODO currentScope.getContext(). */ getObjectName(node.callee) === 'sap.ui.define' ); } function getObjectName(node) { if ( node.type === Syntax.MemberExpression ) { var prefix = getObjectName(node.object); return prefix ? prefix + "." + node.property.name : null; } else if ( node.type === Syntax.Identifier ) { return scope[node.name] ? scope[node.name] : node.name; } else { return null; } } function convertValue(node, type) { var value; if ( node.type === Syntax.Literal ) { // 'string' or number or true or false return node.value; } else if ( node.type === Syntax.UnaryExpression && node.prefix && node.argument.type === Syntax.Literal && typeof node.argument.value === 'number' && ( node.operator === '-' || node.operator === '+' )) { // -n or +n value = node.argument.value; return node.operator === '-' ? -value : value; } else if ( node.type === Syntax.MemberExpression && type ) { // enum value (a.b.c) value = getObjectName(node); if ( value.indexOf(type + ".") === 0 ) { // fully qualified enum name return value.slice(type.length + 1); } else if ( value.indexOf(type.split(".").slice(-1)[0] + ".") === 0 ) { // local name (just a guess - needs static code analysis) return value.slice(type.split(".").slice(-1)[0].length + 1); } else { warning("did not understand default value '%s', falling back to source", value); return value; } } else if ( node.type === Syntax.Identifier && node.name === 'undefined') { // undefined return undefined; } else if ( node.type === Syntax.ArrayExpression && node.elements.length === 0 ) { // empty array literal return "[]"; // TODO return this string or an empty array } error("unexpected type of default value (type='%s', source='%s'), falling back to '???'", node.type, JSON.stringify(node, null, "\t")); return '???'; } function collectClassInfo(extendCall, classDoclet) { var baseType = getObjectName(extendCall.callee.object); var oClassInfo = { metatype: 'control', name: extendCall.arguments[0].value, baseType: baseType, doc: classDoclet && (classDoclet.classdesc || classDoclet.description), deprecation: classDoclet && classDoclet.deprecated, since: classDoclet && classDoclet.since, experimental: classDoclet && classDoclet.experimental, specialSettings : {}, properties: {}, aggregations: {}, associations: {}, events: {}, methods: {} }; function upper(n) { return n.slice(0,1).toUpperCase() + n.slice(1); } function each(node, defaultKey, callback) { var map, n, settings, doclet; map = node && createPropertyMap(node.value); if ( map ) { for (n in map) { if ( map.hasOwnProperty(n) ) { doclet = getLeadingDoclet(map[n]); settings = createPropertyMap(map[n].value, defaultKey); if ( settings == null ) { error("no valid metadata for " + n + " (AST type '" + map[n].value.type + "')"); continue; } callback(n, settings, doclet, map[n]); } } } } var classInfoNode = unlend(extendCall.arguments[1]); var classInfoMap = createPropertyMap(classInfoNode); if ( classInfoMap && classInfoMap.metadata && classInfoMap.metadata.value.type !== Syntax.ObjectExpression ) { warning("class metadata exists but can't be analyzed. It is not of type 'ObjectExpression', but a '" + classInfoMap.metadata.value.type + "'."); return null; } var metadata = classInfoMap && classInfoMap.metadata && createPropertyMap(classInfoMap.metadata.value); if ( metadata ) { verbose(" analyzing metadata for '" + oClassInfo.name + "'"); oClassInfo["abstract"] = !!(metadata["abstract"] && metadata["abstract"].value.value); oClassInfo["final"] = !!(metadata["final"] && metadata["final"].value.value); each(metadata.specialSettings, "readonly", function(n, settings, doclet) { oClassInfo.specialSettings[n] = { name : n, doc : doclet && doclet.description, since : doclet && doclet.since, deprecation : doclet && doclet.deprecated, experimental : doclet && doclet.experimental, visibility : (settings.visibility && settings.visibility.value.value) || "public", type : settings.type ? settings.type.value.value : "any", readonly : (settings.readyonly && settings.readonly.value.value) || true }; }); each(metadata.properties, "type", function (n, settings, doclet) { var type; var N = upper(n); var methods; oClassInfo.properties[n] = { name: n, doc: doclet && doclet.description, since: doclet && doclet.since, deprecation: doclet && doclet.deprecated, experimental: doclet && doclet.experimental, visibility: (settings.visibility && settings.visibility.value.value) || "public", type: (type = settings.type ? settings.type.value.value : "string"), defaultValue: settings.defaultValue ? convertValue(settings.defaultValue.value, type) : null, group : settings.group ? settings.group.value.value : 'Misc', bindable : settings.bindable ? !!convertValue(settings.bindable.value) : false, methods: (methods = { "get": "get" + N, "set": "set" + N }) }; if ( oClassInfo.properties[n].bindable ) { methods["bind"] = "bind" + N; methods["unbind"] = "unbind" + N; } }); oClassInfo.defaultAggregation = (metadata.defaultAggregation && metadata.defaultAggregation.value.value) || undefined; each(metadata.aggregations, "type", function (n, settings, doclet) { var N = upper(n); var methods; oClassInfo.aggregations[n] = { name: n, doc: doclet && doclet.description, deprecation: doclet && doclet.deprecated, since: doclet && doclet.since, experimental: doclet && doclet.experimental, visibility: (settings.visibility && settings.visibility.value.value) || "public", type: settings.type ? settings.type.value.value : "sap.ui.core.Control", singularName: settings.singularName ? settings.singularName.value.value : guessSingularName(n), cardinality: (settings.multiple && !settings.multiple.value.value) ? "0..1" : "0..n", bindable : settings.bindable ? !!convertValue(settings.bindable.value) : false, methods: (methods = { "get": "get" + N, "destroy": "destroy" + N }) }; if ( oClassInfo.aggregations[n].cardinality === "0..1" ) { methods["set"] = "set" + N; } else { var N1 = upper(oClassInfo.aggregations[n].singularName); methods["insert"] = "insert" + N1; methods["add"] = "add" + N1; methods["remove"] = "remove" + N1; methods["indexOf"] = "indexOf" + N1; methods["removeAll"] = "removeAll" + N; } if ( oClassInfo.aggregations[n].bindable ) { methods["bind"] = "bind" + N; methods["unbind"] = "unbind" + N; } }); each(metadata.associations, "type", function (n, settings, doclet) { var N = upper(n); var methods; oClassInfo.associations[n] = { name: n, doc: doclet && doclet.description, deprecation: doclet && doclet.deprecated, since: doclet && doclet.since, experimental: doclet && doclet.experimental, visibility: (settings.visibility && settings.visibility.value.value) || "public", type: settings.type ? settings.type.value.value : "sap.ui.core.Control", singularName: settings.singularName ? settings.singularName.value.value : guessSingularName(n), cardinality : (settings.multiple && settings.multiple.value.value) ? "0..n" : "0..1", methods: (methods = { "get": "get" + N }) }; if ( oClassInfo.associations[n].cardinality === "0..1" ) { methods["set"] = "set" + N; } else { var N1 = upper(oClassInfo.associations[n].singularName); methods["add"] = "add" + N1; methods["remove"] = "remove" + N1; methods["removeAll"] = "removeAll" + N; } }); each(metadata.events, null, function (n, settings, doclet) { var N = upper(n); var info = oClassInfo.events[n] = { name: n, doc: doclet && doclet.description, deprecation: doclet && doclet.deprecated, since: doclet && doclet.since, experimental: doclet && doclet.experimental, allowPreventDefault: !!(settings.allowPreventDefault && settings.allowPreventDefault.value.value), parameters : {}, methods: { "attach": "attach" + N, "detach": "detach" + N, "fire": "fire" + N } }; each(settings.parameters, null, function (pName, pSettings, pDoclet) { info.parameters[pName] = { name: pName, doc: pDoclet && pDoclet.description, deprecation: pDoclet && pDoclet.deprecated, since: pDoclet && pDoclet.since, experimental: pDoclet && pDoclet.experimental, type: pSettings && pSettings.type ? pSettings.type.value.value : "" }; }); }); } return oClassInfo; } function collectEnumInfo(node) { var doclet = Doclet.get(node); var name = /* TODO currentScope.getContext(). */ getObjectName(node.expression.left); if ( name && doclet && doclet.isPublic() ) { var oTypeDoc = { metatype: "type", doc: undefined, deprecation: false, visibility: 'public' }; oTypeDoc.name = name; if ( doclet ) { oTypeDoc.doc = doclet.description; oTypeDoc.deprecation = doclet.deprecation; oTypeDoc.since = doclet.since; oTypeDoc.experimental = doclet.experimental; // TODO oTypeDoc["final"] = doclet.hasTatypeDocumentation.hasTag("final") ? new SimpleType.Final() : null); // TODO simpleType.setDefaultValue(typeDocumentation.getTagContent("defaultvalue")); } var properties = node.expression.right.properties || []; oTypeDoc.values = {}; for (var i = 0; i < properties.length; i++) { // documentation must precede the name/value pair var propDoclet = Doclet.get(properties[i]); var key = properties[i].key; var value = properties[i].value; var valueInfo = {}; // the name of the enum value equals the name in the name/value pair if ( key.type == Syntax.Identifier ) { valueInfo.name = key.name; } else if ( key.type == Syntax.Literal ) { valueInfo.name = key.value; } else { throw new Error(); } // the value equals the value in the name/value pair if ( value.type == Syntax.Literal ) { valueInfo.value = value.value; } else { throw new Error(); } if ( propDoclet != null ) { valueInfo.doc = propDoclet.description; valueInfo.deprecation = propDoclet.deprecation; valueInfo.since = propDoclet.since; valueInfo.experimental = propDoclet.experimental; } oTypeDoc.values[valueInfo.name] = valueInfo; } aInfos.push(oTypeDoc); } } function collectRegExTypeInfo(node) { var doclet = Doclet.get(node); var name = node.expression.right.arguments[0].value; var settings = ASTUtils.createPropertyMap(node.expression.right.arguments[1]); var baseType = null; if ( node.expression.right.arguments.length > 2 && node.expression.right.arguments[2].type == Syntax.CallExpression && node.expression.right.arguments[2].callee.type == Syntax.MemberExpression && /* TODO currentScope.getContext().*/ getObjectName(node.expression.right.arguments[2].callee) == "sap.ui.base.DataType.getType" && node.expression.right.arguments[2].arguments.length > 0 && node.expression.right.arguments[2].arguments[0].type == Syntax.Literal ) { baseType = node.expression.right.arguments[2].arguments[0].value; } if ( name && doclet && doclet.isPublic() ) { var oTypeDoc = { metatype: "type", doc: undefined, deprecation: false, visibility: 'public' }; oTypeDoc.name = name; if ( doclet ) { oTypeDoc.doc = doclet.description; oTypeDoc.deprecation = doclet.deprecation; oTypeDoc.since = doclet.since; oTypeDoc.experimental = doclet.experimental; // TODO oTypeDoc["final"] = doclet.hasTatypeDocumentation.hasTag("final") ? new SimpleType.Final() : null); } var defaultValue = settings.defaultValue; if ( defaultValue ) { oTypeDoc.defaultValue = convertValue(defaultValue.value, name); } var isValid = settings.isValid; if ( isValid && isValid.value.type == Syntax.FunctionExpression && isValid.value.body && isValid.value.body.body.length > 0 && isValid.value.body.body[0].type == Syntax.ReturnStatement && isValid.value.body.body[0].argument.type == Syntax.CallExpression && isValid.value.body.body[0].argument.callee.type == Syntax.MemberExpression && isValid.value.body.body[0].argument.callee.object.type == Syntax.Literal && isValid.value.body.body[0].argument.callee.object.value instanceof RegExp ) { var pattern = isValid.value.body.body[0].argument.callee.object.value.source; if ( /^\^\(.*\)\$$/.test(pattern) ) { pattern = pattern.slice(2, -2); } oTypeDoc.pattern = pattern; } oTypeDoc.baseType = baseType; aInfos.push(oTypeDoc); } } function resolveRelativeDependency(dep) { return /^\.\//.test(dep) ? currentPackage + dep.slice(1) : dep; } /** * Get the documentation information needed for a given parameter * @param {string} sParamName Name of the parameter to be fetched * @param {array} aDocTags With documentation tags * @return {Object} Parameter information */ function getParamInfo(sParamName, aDocTags) { //set default parameter type if there are no @ definitions for the type var sParamType = '', sParamDescription = '', iParamNameIndex, iDocStartIndex, rEgexMatchType = /{(.*)}/, aMatch; for (var i = 0; i < aDocTags.length; i++) { if ( aDocTags[i].tag !== 'param' ) { continue; } aMatch = rEgexMatchType.exec(aDocTags[i].content); iParamNameIndex = aDocTags[i].content.indexOf(sParamName); if ( aMatch && iParamNameIndex > -1 ) { //get the match without the curly brackets sParamType = aMatch[1]; iDocStartIndex = iParamNameIndex + sParamName.length; sParamDescription = aDocTags[i].content.substr(iDocStartIndex); //clean the doc from - symbol if they come after the param name and trim the extra whitespace sParamDescription = sParamDescription.replace(/[-]/, '').trim(); // prevent unnecessary looping! break; } } return { name: sParamName, type: sParamType, doc: sParamDescription }; } var delegate = { "ExpressionStatement": function (node) { if ( isSapUiDefineCall(node.expression) ) { var i = 0; var dependencies, factory; if ( i < node.expression.arguments.length && node.expression.arguments[i].type === Syntax.Literal ) { /* name = */ node.expression.arguments[i++].value; } if ( i < node.expression.arguments.length && node.expression.arguments[i].type === Syntax.ArrayExpression ) { dependencies = node.expression.arguments[i++].elements; } if ( i < node.expression.arguments.length && node.expression.arguments[i].type === Syntax.FunctionExpression ) { factory = node.expression.arguments[i++]; } // // unused // if ( i < node.expression.arguments.length && node.expression.arguments[i].type === Syntax.FunctionExpression ) { // export_ = node.expression.arguments[i++]; // } if ( dependencies && factory && factory.params ) { for (var j = 0; j < dependencies.length; j++) { var dep = dependencies[j].type === Syntax.Literal ? resolveRelativeDependency(dependencies[j].value) : null; var paramName = j < factory.params.length ? factory.params[j].name : null; if ( dep && paramName ) { // TODO this is only a hack. For a proper scope and constant value handling // much more needs to be done (e.g. migration of StaticAnalyzer.java) scope[paramName] = dep.replace(/\//g, '.'); } } } } // ---- Something = { ... } ---- if ( node.expression.type == Syntax.AssignmentExpression && node.expression.right.type == Syntax.ObjectExpression && node.expression.left.type == Syntax.MemberExpression ) { collectEnumInfo(node); } // ---- sap.ui.base.DataType.createType ---- if ( node.expression.type === Syntax.AssignmentExpression && node.expression.right.type === Syntax.CallExpression && node.expression.right.callee.type === Syntax.MemberExpression && node.expression.right.callee.property.type === Syntax.Identifier && node.expression.right.callee.property.name === 'createType' && /* TODO currentScope.getContext().*/ getObjectName(node.expression.right.callee.object) == 'sap.ui.base.DataType' && node.expression.right.arguments.length >= 2 && node.expression.right.arguments[0].type === Syntax.Literal && node.expression.right.arguments[1].type === Syntax.ObjectExpression ) { collectRegExTypeInfo(node); } // ---- Something.extend() ---- if ( isExtendCall(node.expression) ) { var doclet = Doclet.get(node) || Doclet.get(node.expression); var oClassInfo = collectClassInfo(node.expression, doclet); if ( oClassInfo ) { aInfos.push(oClassInfo); } } }, "VariableDeclaration": function (node) { if ( node.declarations.length == 1 && node.declarations[0].init && isExtendCall(node.declarations[0].init) ) { var doclet = Doclet.get(node) || Doclet.get(node.declarations[0]); var oClassInfo = collectClassInfo(node.declarations[0].init, doclet); if ( oClassInfo ) { aInfos.push(oClassInfo); } } }, "FunctionExpression": function (node) { var aFunctions = node.body.body; aFunctions.forEach(function (functionNode) { if ( functionNode.expression && functionNode.expression.type !== Syntax.AssignmentExpression ) { return; } var oParsed = functionNode; var oFuncDoc = Doclet.get(functionNode); if ( oFuncDoc && oFuncDoc.isPublic() && oParsed.expression && oParsed.expression.left && oParsed.expression.left.property && oParsed.expression.left.object && oParsed.expression.left.object.object && oParsed.expression.left.object.object.name === sControlName ) { var oPublicMethod = {}, parsedParams = oParsed.expression.right.params ? oParsed.expression.right.params : []; oPublicMethod.name = oParsed.expression.left.property.name; oPublicMethod.doc = oFuncDoc.description; oPublicMethod.since = oFuncDoc.since; oPublicMethod.experimental = oFuncDoc.experimental; oPublicMethod.deprecation = oFuncDoc.deprecated; oPublicMethod.type = oFuncDoc.type ? oFuncDoc.type : ''; oPublicMethod.parameters = []; parsedParams.forEach(function (oParam) { var oParamData = getParamInfo(oParam.name, oFuncDoc.tags); oPublicMethod.parameters.push(oParamData); }); oPublicFunctions[oPublicMethod.name] = oPublicMethod; } }); } }; /** * Adds the methods section to the metadata * @param {Object} metadata */ // it should be done at the latest because sometimes it is empty on collectClassInfo // and it does not call collectClassInfo or createPropertyMap by itself function addMethodsToMetadata(metadata) { metadata.methods = oPublicFunctions; } function analyze(oData, sEntityName, sModuleName) { currentPackage = sModuleName.split('/').slice(0, -1).join('/'); aInfos = []; oPublicFunctions = {}; scope = {}; sControlName = sEntityName.split('.').pop(); var ast = esprima.parse(oData, {comment: true, attachComment: true}); ASTUtils.visit(ast, delegate); for (var i = 0; i < aInfos.length; i++) { if ( aInfos[i].name === sEntityName ) { addMethodsToMetadata(aInfos[i]); return aInfos[i]; } } } return { analyze: analyze }; }, false);