From c868fa0d0a4c46d6c3098785a23fee3b7097cf02 Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Tue, 19 May 2020 17:20:41 +0200 Subject: [PATCH] [FIX] Align JSDoc template and scripts with OpenUI5 1.79 (#460) --- lib/processors/jsdoc/lib/createIndexFiles.js | 76 ++- lib/processors/jsdoc/lib/transformApiJson.js | 127 +++-- lib/processors/jsdoc/lib/ui5/plugin.js | 110 ++++- .../jsdoc/lib/ui5/template/publish.js | 455 ++++++++++++++---- 4 files changed, 614 insertions(+), 154 deletions(-) diff --git a/lib/processors/jsdoc/lib/createIndexFiles.js b/lib/processors/jsdoc/lib/createIndexFiles.js index a0f5b7821..f258e16a6 100644 --- a/lib/processors/jsdoc/lib/createIndexFiles.js +++ b/lib/processors/jsdoc/lib/createIndexFiles.js @@ -1,7 +1,7 @@ /* * Node script to create cross-library API index files for use in the UI5 SDKs. * - * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company. + * (c) Copyright 2009-2020 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ @@ -248,13 +248,12 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile let aTree = []; // Filter out black list libraries - symbols = symbols.filter(({lib}) => ["sap.ui.demokit", "sap.ui.documentation"].indexOf(lib) === -1); + symbols = symbols.filter(({lib}) => ["sap.ui.documentation"].indexOf(lib) === -1); // Create treeName and displayName symbols.forEach(oSymbol => { oSymbol.treeName = oSymbol.name.replace(/^module:/, "").replace(/\//g, "."); oSymbol.displayName = oSymbol.treeName.split(".").pop(); - oSymbol.bIsDeprecated = !!oSymbol.deprecated; }); // Create missing - virtual namespaces @@ -269,8 +268,7 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile displayName: sPart, lib: oSymbol.lib, kind: "namespace", - visibility: "public", // Virtual namespace are always public - bIsDeprecated: false // Virtual namespace can't be deprecated + visibility: "public" // Virtual namespace are always public }); } }); @@ -326,6 +324,12 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile return 0; }); + // walk the tree *from bottom to top* + // in order to detect all parent nodes + // that should be marked as content-deprecated + // because their children are explicitly deprecated + toChildrenFirstArray(aTree).forEach(markDeprecatedNodes); + // Clean tree - keep file size down function cleanTree (oSymbol) { delete oSymbol.treeName; @@ -339,6 +343,68 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile return aTree; } + /** + * Creates an array of all tree nodes, + * where the child nodes precede the parent nodes + * @param aTree + * @returns {Array} + */ + function toChildrenFirstArray(aTree) { + var aChildrenFirst = []; + function addToLeafsFirst(node) { + if (node.nodes) { + node.nodes.forEach(function(child) { + addToLeafsFirst(child); + }); + } + aChildrenFirst.push(node); + } + aTree.forEach(function(parent) { + addToLeafsFirst(parent); + }); + return aChildrenFirst; + } + + /** + * Sets the bAllContentDeprecated flag of each symbol + * + * The bAllContentDeprecated flag differs from the already existing deprecated flag + * in the following respect: + * + * 1) if a node is deprecated => all its children should be marked as bAllContentDeprecated + * (even if not explicitly deprecated in their JSDoc) + * 2) if all children of the node are deprecated => that node should also be marked as bAllContentDeprecated + * (even if not explicitly deprecated in its JSDoc) + * 3) if a node is explicitly deprecated in its JSDoc => it should also be marked as bAllContentDeprecated + * (for consistency) + * + * @param oSymbol + */ + function markDeprecatedNodes(oSymbol) { + // 1. If the symbol is deprecated all content in it should be also deprecated + if (oSymbol.deprecated) { + // 2. If all content in the symbol is deprecated, flag should explicitly be passed to its child nodes. + propagateFlags(oSymbol, { bAllContentDeprecated: true }); + } else { + // 3. If all children are deprecated, then the parent is marked as content-deprecated + oSymbol.bAllContentDeprecated = !!oSymbol.nodes && oSymbol.nodes.every(node => node.bAllContentDeprecated); + } + } + + /** + * Merges the set of flags from oFlags into the given oSymbol + * @param oSymbol + * @param oFlags + */ + function propagateFlags(oSymbol, oFlags) { + Object.assign(oSymbol, oFlags); + if (oSymbol.nodes) { + oSymbol.nodes.forEach(node => { + propagateFlags(node, oFlags); + }) + } + } + function createOverallIndex() { let version = "0.0.0"; const filesToReturn = {}; diff --git a/lib/processors/jsdoc/lib/transformApiJson.js b/lib/processors/jsdoc/lib/transformApiJson.js index 7071d4c08..af8585e20 100644 --- a/lib/processors/jsdoc/lib/transformApiJson.js +++ b/lib/processors/jsdoc/lib/transformApiJson.js @@ -1,7 +1,7 @@ /* * Node script to preprocess api.json files for use in the UI5 SDKs. * - * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company. + * (c) Copyright 2009-2020 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ @@ -132,16 +132,24 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Attach children info symbols.forEach(oSymbol => { if (oSymbol.parent) { - let oParent = symbols.find(({treeName}) => treeName === oSymbol.parent); + let oParent = symbols.find(({treeName}) => treeName === oSymbol.parent), + oNode; if (!oParent.nodes) oParent.nodes = []; - oParent.nodes.push({ + + oNode = { name: oSymbol.displayName, description: formatters._preProcessLinksInTextBlock( extractFirstSentence(oSymbol.description) ), - href: "#/api/" + encodeURIComponent(oSymbol.name) - }); + href: "api/" + encodeURIComponent(oSymbol.name) + }; + + if (oSymbol.deprecated) { + oNode.deprecated = true; + } + + oParent.nodes.push(oNode); } }); @@ -308,7 +316,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Link Enabled if (oSymbol.kind !== "enum" && !isBuiltInType(oProperty.type) && possibleUI5Symbol(oProperty.type)) { oProperty.linkEnabled = true; - oProperty.href = "#/api/" + oProperty.type.replace("[]", ""); + oProperty.href = "api/" + oProperty.type.replace("[]", ""); } // Keep file size in check @@ -392,7 +400,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oAggregation.description = formatters.formatDescription(oAggregation.description, oAggregation.deprecated.text, oAggregation.deprecated.since); } else { - oAggregation.description = formatters.formatDescription(oAggregation.description); + oAggregation.description = formatters.formatDescriptionSince(oAggregation.description, oAggregation.since); } // Link enabled @@ -426,13 +434,39 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oAssociation.description = formatters.formatDescription(oAssociation.description, oAssociation.deprecated.text, oAssociation.deprecated.since); } else { - oAssociation.description = formatters.formatDescription(oAssociation.description); + oAssociation.description = formatters.formatDescriptionSince(oAssociation.description, oAssociation.since); } }); } // Events if (oMeta.events) { + + oMeta.events.forEach(oEvent => { + let aParams = oEvent.parameters; + + aParams && Object.keys(aParams).forEach(sParam => { + let sSince = aParams[sParam].since; + let oDeprecated = aParams[sParam].deprecated; + let oEvtInSymbol = oSymbol.events.find(e => e.name === oEvent.name); + let oParamInSymbol = oEvtInSymbol && oEvtInSymbol.parameters[0] && + oEvtInSymbol.parameters[0].parameterProperties && + oEvtInSymbol.parameters[0].parameterProperties.getParameters && + oEvtInSymbol.parameters[0].parameterProperties.getParameters.parameterProperties && + oEvtInSymbol.parameters[0].parameterProperties.getParameters.parameterProperties[sParam]; + + if (typeof oParamInSymbol === 'object' && oParamInSymbol !== null) { + if (sSince) { + oParamInSymbol.since = sSince; + } + + if (oDeprecated) { + oParamInSymbol.deprecated = oDeprecated; + } + } + }) + }); + // We don't need event's data from the UI5-metadata for now. Keep file size in check delete oMeta.events; } @@ -508,13 +542,15 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, } // Description - if (oParameter.deprecated) { - oParameter.description = formatters.formatDescription(oParameter.description, - oParameter.deprecated.text, oParameter.deprecated.since); - } else { - oParameter.description = formatters.formatDescription(oParameter.description); + if (oParameter.description) { + oParameter.description = formatters.formatDescriptionSince(oParameter.description, oParameter.since); } + // Deprecated + if (oParameter.deprecated) { + oParameter.deprecatedText = formatters.formatDeprecated(oParameter.deprecated.since, + oParameter.deprecated.text); + } }); } @@ -535,8 +571,8 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oMethod.name = formatters.formatEntityName(oMethod.name, oSymbol.name, oMethod.static); // Link - oMethod.href = "#/api/" + encodeURIComponent(oSymbol.name) + - "/methods/" + encodeURIComponent(oMethod.name); + oMethod.href = "api/" + oSymbol.name + + "#methods/" + oMethod.name; } formatters.formatReferencesInDescription(oMethod); @@ -571,7 +607,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Link Enabled if (!isBuiltInType(oType.value) && possibleUI5Symbol(oType.value)) { oType.linkEnabled = true; - oType.href = "#/api/" + oType.value.replace("[]", ""); + oType.href = "api/" + oType.value.replace("[]", ""); } }); @@ -603,7 +639,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Link Enabled if (!isBuiltInType(oType.value)) { - oType.href = "#/api/" + encodeURIComponent(oType.value.replace("[]", "")); + oType.href = "api/" + oType.value.replace("[]", ""); oType.linkEnabled = true; } @@ -993,11 +1029,13 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, * @returns string - the formatted text */ formatAnnotationNamespace: function (namespace) { - var result, - aNamespaceParts = namespace.split("."); + var aNamespaceParts = namespace.split("."), + result, target, text; if (aNamespaceParts[0] === "Org" && aNamespaceParts[1] === "OData") { - result = '' + namespace + ''; + target = this.ANNOTATIONS_NAMESPACE_LINK + namespace + ".xml"; + text = namespace; + result = this.handleExternalUrl(target, text); } else { result = namespace; } @@ -1110,15 +1148,15 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, var sReturn; switch (defaultValue) { - case null: - case undefined: - sReturn = ''; - break; - case '': - sReturn = 'empty string'; - break; - default: - sReturn = defaultValue; + case null: + case undefined: + sReturn = ''; + break; + case '': + sReturn = 'empty string'; + break; + default: + sReturn = defaultValue; } return Array.isArray(sReturn) ? sReturn.join(', ') : sReturn; @@ -1313,7 +1351,7 @@ title="Information published on ${bSAPHosted ? '' : 'non '}SAP site" class="sapU let oProperty = findProperty(oSymbol, methodName, target); if (oProperty) { sResult = this.createLink({ - name: oProperty.name, + name: className, text: text }); return true; @@ -1409,21 +1447,18 @@ title="Information published on ${bSAPHosted ? '' : 'non '}SAP site" class="sapU name = name.replace(/^module:/, ""); } - name = encodeURIComponent(name); - className = encodeURIComponent(className); - // Build the link - sLink = type ? `${className}/${type}/${name}` : name; + sLink = type ? `${className}#${type}/${name}` : name; if (hrefAppend) { sLink += hrefAppend; } if (local) { - return `${text}`; + return `${text}`; } - return `${text}`; + return `${text}`; }, /** @@ -1453,7 +1488,7 @@ title="Information published on ${bSAPHosted ? '' : 'non '}SAP site" class="sapU // topic:xxx Topic aMatch = sTarget.match(/^topic:(\w{32})$/); if (aMatch) { - return '' + sText + ''; + return '' + sText + ''; } // sap.x.Xxx.prototype.xxx - In case of prototype we have a link to method @@ -1499,7 +1534,7 @@ title="Information published on ${bSAPHosted ? '' : 'non '}SAP site" class="sapU return this.createLink({ name: sName, - hrefAppend: "/constructor", + hrefAppend: "#constructor", text: sText }); } @@ -1974,14 +2009,14 @@ title="Information published on ${bSAPHosted ? '' : 'non '}SAP site" class="sapU // Start the work here let p = getLibraryPromise(oChainObject) - .then(extractComponentAndDocuindexUrl) - .then(flattenComponents) - .then(extractSamplesFromDocuIndex) - .then(getDependencyLibraryFilesList) - .then(getAPIJSONPromise) - .then(loadDependencyLibraryFiles) - .then(transformApiJson) - .then(createApiRefApiJson); + .then(extractComponentAndDocuindexUrl) + .then(flattenComponents) + .then(extractSamplesFromDocuIndex) + .then(getDependencyLibraryFilesList) + .then(getAPIJSONPromise) + .then(loadDependencyLibraryFiles) + .then(transformApiJson) + .then(createApiRefApiJson); return p; }; diff --git a/lib/processors/jsdoc/lib/ui5/plugin.js b/lib/processors/jsdoc/lib/ui5/plugin.js index ce18e7a9e..1f5eef9dd 100644 --- a/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/lib/processors/jsdoc/lib/ui5/plugin.js @@ -1,7 +1,7 @@ /* * JSDoc3 plugin for UI5 documentation generation. * - * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company. + * (c) Copyright 2009-2020 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ @@ -56,7 +56,7 @@ var Syntax = require('jsdoc/src/syntax').Syntax; var Doclet = require('jsdoc/doclet').Doclet; var fs = require('jsdoc/fs'); var path = require('jsdoc/path'); -var pluginConfig = (env.conf && env.conf.templates && env.conf.templates.ui5) || {}; +var pluginConfig = (env.conf && env.conf.templates && env.conf.templates.ui5) || env.opts.sapui5 || {}; /* ---- global vars---- */ @@ -99,7 +99,7 @@ var currentProgram; * have a 'module' and optionally a 'path' value. In that case, the local name represents an AMD * module import or a shortcut derived from such an import. * - * See {@link getREsolvedObjectName} how the knowledge about locale names is used. + * See {@link getResolvedObjectName} how the knowledge about locale names is used. * * @type {{name:string,resource:string,module:string,localName:Object}} */ @@ -117,6 +117,17 @@ var currentSource; */ var classInfos = Object.create(null); +/** + * Map of enum value objects keyed by a unqiue enum ID. + * + * When the AST visitor detects an object literal that might be an enum, it cannot easily determine + * the name of the enum. Therefore, the collected enum values are stored in this map keyed by a + * unique ID derived from the key set of the (potential) enum (ID = sorted key set, joined with '|'). + * + * In the parseComplete phase, the found values are merged into enum symbols (as detected by JSDoc). + */ +var enumValues = Object.create(null); + /** * */ @@ -281,7 +292,7 @@ function analyzeModuleDefinition(node) { /** * Searches the given body for variable declarations that can be evaluated statically, - * either because they refer to known AMD modukle imports (e.g. shortcut varialbes) + * either because they refer to known AMD module imports (e.g. shortcut variables) * or because they have a (design time) constant value. * * @param {ASTNode} body AST node of a function body that shall be searched for shortcuts @@ -494,6 +505,19 @@ function isProbingRequireCall(node) { ); } +function isPotentialEnum(node) { + if ( node == null || node.type !== Syntax.ObjectExpression ) { + return false; + } + return node.properties.every(function(prop) { + return isCompileTimeConstant(prop.value); + }); +} + +function isCompileTimeConstant(node) { + return node && node.type === Syntax.Literal; +} + function getObjectName(node) { if ( node.type === Syntax.MemberExpression && !node.computed && node.property.type === Syntax.Identifier ) { var prefix = getObjectName(node.object); @@ -686,7 +710,8 @@ function collectClassInfo(extendCall, classDoclet) { methods : {}, annotations : {}, designtime: false, - stereotype: null + stereotype: null, + metadataClass: undefined }; function upper(n) { @@ -713,6 +738,17 @@ function collectClassInfo(extendCall, classDoclet) { } } + if ( extendCall.arguments.length > 2 ) { + // new class defines its own metadata class type + var metadataClass = getResolvedObjectName(extendCall.arguments[2]); + if ( metadataClass ) { + oClassInfo.metadataClass = getResolvedObjectName(extendCall.arguments[2]); + debug("found metadata class name '" + oClassInfo.metadataClass + "'"); + } else { + error("cannot understand metadata class parameter (AST node type '" + extendCall.arguments[2] + "')"); + } + } + var classInfoNode = extendCall.arguments[1]; var classInfoMap = createPropertyMap(classInfoNode); if ( classInfoMap && classInfoMap.metadata && classInfoMap.metadata.value.type !== Syntax.ObjectExpression ) { @@ -1211,6 +1247,25 @@ function createAutoDoc(oClassInfo, classComment, node, parser, filename, comment return prefix + n.slice(0,1).toUpperCase() + n.slice(1); } + function generateParamTag(n, type, description, defaultValue){ + var s = "@param {" + info.type + "} "; + + if (defaultValue !== null) { + s += "[" + varname(n, type, true) + "="; + if (type === "string"){ + defaultValue = "\"" + defaultValue + "\""; + } + s += defaultValue + "]"; + + } else { + s += varname(n, type, true); + } + + s += " " + description; + + return s; + } + // add a list of the possible settings if and only if // - documentation for the constructor exists // - no (generated) documentation for settings exists already @@ -1357,7 +1412,7 @@ function createAutoDoc(oClassInfo, classComment, node, parser, filename, comment "", "@param {string} sClassName Name of the class being created", "@param {object} [oClassInfo] Object literal with information about the class", - "@param {function} [FNMetaImpl] Constructor function for the metadata object; if not given, it defaults to sap.ui.core.ElementMetadata", + "@param {function} [FNMetaImpl] Constructor function for the metadata object; if not given, it defaults to the metadata implementation used by this class", "@returns {function} Created class / constructor function", "@public", "@static", @@ -1395,7 +1450,7 @@ function createAutoDoc(oClassInfo, classComment, node, parser, filename, comment "When called with a value of null or undefined, the default value of the property will be restored.", "", info.defaultValue !== null ? "Default value is " + (info.defaultValue === "" ? "empty string" : info.defaultValue) + "." : "", - "@param {" + info.type + "} " + varname(n,info.type,true) + " New value for property " + n + "", + generateParamTag(n, info.type, "New value for property " + n + "", info.defaultValue), "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", info.since ? "@since " + info.since : "", info.deprecation ? "@deprecated " + info.deprecation : "", @@ -2278,6 +2333,28 @@ exports.handlers = { }; } + if ( doclet.kind === 'member' && doclet.isEnum && Array.isArray(doclet.properties) ) { + // determine unique enum identifier from key set + var enumID = doclet.properties.map(function(prop) { + return prop.name; + }).sort().join("|"); + if ( enumValues[enumID] ) { + // debug("found enum values for ", enumID, enumValues[enumID]); + var standardEnum = true; + /* eslint-disable no-loop-func */ + doclet.properties.forEach(function(prop) { + prop.__ui5.value = enumValues[enumID][prop.name]; + if ( prop.__ui5.value !== prop.name ) { + standardEnum = false; + } + }); + /* eslint-enable no-loop-func */ + if ( standardEnum ) { + doclet.__ui5.stereotype = 'enum'; + } + } + } + // check for duplicates: last one wins if ( j > 0 && doclets[j - 1].longname === doclet.longname ) { if ( !doclets[j - 1].synthetic && !doclet.__ui5.updatedDoclet ) { @@ -2331,6 +2408,18 @@ exports.astNodeVisitor = { } } + function processPotentialEnum(literal, comment) { + var values = literal.properties.reduce(function(map, prop) { + map[getPropertyKey(prop)] = convertValue(prop.value); + return map; + }, Object.create(null)); + // determine unique enum ID from key set + var enumID = Object.keys(values).sort().join("|"); + // and remember the values with that ID + enumValues[enumID] = values; + // debug("found enum values for key-set", enumID); + } + if ( node.type === Syntax.ExpressionStatement ) { if ( isSapUiDefineCall(node.expression) ) { analyzeModuleDefinition(node.expression); @@ -2372,6 +2461,9 @@ exports.astNodeVisitor = { comment = (idx === 0 ? getLeadingCommentNode(node) : undefined) || getLeadingCommentNode(decl); // console.log("ast node with comment " + comment); processExtendCall(decl.init, comment); + } else if ( isPotentialEnum(decl.init) ) { + comment = (idx === 0 ? getLeadingCommentNode(node) : undefined) || getLeadingCommentNode(decl); + processPotentialEnum(decl.init, comment); } }); @@ -2392,6 +2484,10 @@ exports.astNodeVisitor = { processDataType(node.expression.right); // TODO remember knowledge about type and its name (left hand side of assignment) + } else if ( isPotentialEnum(node.expression.right) ) { + comment = getLeadingCommentNode(node) || getLeadingCommentNode(node.expression); + // console.log(getResolvedObjectName(node.expression.left)); + processPotentialEnum(node.expression.right, comment); } } diff --git a/lib/processors/jsdoc/lib/ui5/template/publish.js b/lib/processors/jsdoc/lib/ui5/template/publish.js index c7a08d445..ac619c469 100644 --- a/lib/processors/jsdoc/lib/ui5/template/publish.js +++ b/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -1,7 +1,7 @@ /* * JSDoc3 template for UI5 documentation generation. * - * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company. + * (c) Copyright 2009-2020 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ @@ -19,6 +19,7 @@ var template = require('jsdoc/template'), /* globals, constants */ var MY_TEMPLATE_NAME = "ui5", + MY_ALT_TEMPLATE_NAME = "sapui5-jsdoc3", ANONYMOUS_LONGNAME = doclet.ANONYMOUS_LONGNAME, A_SECURITY_TAGS = [ { @@ -56,8 +57,9 @@ var MY_TEMPLATE_NAME = "ui5", var rSecurityTags = new RegExp(A_SECURITY_TAGS.map(function($) {return $.name.toLowerCase(); }).join('|'), "i"); //debug(A_SECURITY_TAGS.map(function($) {return $.name; }).join('|')); -var templateConf = (env.conf.templates || {})[MY_TEMPLATE_NAME] || {}, - pluginConf = templateConf, +var templatesConf = (env.conf.templates || {}), + templateConf = templatesConf[MY_TEMPLATE_NAME] || templatesConf[MY_ALT_TEMPLATE_NAME] || {}, + pluginConf = templateConf, conf = {}, view; @@ -580,50 +582,51 @@ function publish(symbolSet) { var now = new Date(); info("start publishing"); - for (var i = 0; i < templateConf.variants.length; i++) { + if ( Array.isArray(templateConf.variants) ) { + templateConf.variants.forEach(function(vVariant) { - var vVariant = templateConf.variants[i]; - if ( typeof vVariant === "string" ) { - vVariant = { variant : vVariant }; - } - - info(""); - - if ( PUBLISHING_VARIANTS[vVariant.variant] ) { - - // Merge different sources of configuration (listed in increasing priority order - last one wins) - // and expose the result in the global 'conf' variable - // - global defaults - // - defaults for current variant - // - user configuration for sapui5 template - // - user configuration for current variant - // - // Note: trailing slash expected for dirs - conf = merge({ - ext: ".html", - filter: function($) { return true; }, - templatesDir: "/templates/sapui5/", - symbolsDir: "symbols/", - modulesDir: "modules/", - topicUrlPattern: "../../guide/{{topic}}.html", - srcDir: "symbols/src/", - creationDate : now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDay() + " " + now.getHours() + ":" + now.getMinutes(), - outdir: env.opts.destination - }, PUBLISHING_VARIANTS[vVariant.variant].defaults, templateConf, vVariant); - - info("publishing as variant '" + vVariant.variant + "'"); - debug("final configuration:"); - debug(conf); - - PUBLISHING_VARIANTS[vVariant.variant].processor(conf); + if ( typeof vVariant === "string" ) { + vVariant = { variant : vVariant }; + } - info("done with variant " + vVariant.variant); + info(""); + + if ( PUBLISHING_VARIANTS[vVariant.variant] ) { + + // Merge different sources of configuration (listed in increasing priority order - last one wins) + // and expose the result in the global 'conf' variable + // - global defaults + // - defaults for current variant + // - user configuration for sapui5 template + // - user configuration for current variant + // + // Note: trailing slash expected for dirs + conf = merge({ + ext: ".html", + filter: function($) { return true; }, + templatesDir: "/templates/sapui5/", + symbolsDir: "symbols/", + modulesDir: "modules/", + topicUrlPattern: "../../guide/{{topic}}.html", + srcDir: "symbols/src/", + creationDate : now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDay() + " " + now.getHours() + ":" + now.getMinutes(), + outdir: env.opts.destination + }, PUBLISHING_VARIANTS[vVariant.variant].defaults, templateConf, vVariant); + + info("publishing as variant '" + vVariant.variant + "'"); + debug("final configuration:"); + debug(conf); + + PUBLISHING_VARIANTS[vVariant.variant].processor(conf); + + info("done with variant " + vVariant.variant); - } else { + } else { - info("cannot publish unknown variant '" + vVariant.variant + "' (ignored)"); + info("cannot publish unknown variant '" + vVariant.variant + "' (ignored)"); - } + } + }); } var builtinSymbols = templateConf.builtinSymbols; @@ -741,17 +744,23 @@ function createInheritanceTree() { function getOrCreateClass(sClass, sExtendingClass) { var oClass = lookup(sClass); if ( !oClass ) { - warning("create missing class " + sClass + " (extended by " + sExtendingClass + ")"); - var sBaseClass = 'Object'; + var sKind = "class", + sBaseClass = 'Object', + sVisibility = "public"; if ( externalSymbols[sClass] ) { + sKind = externalSymbols[sClass].kind || sKind; sBaseClass = externalSymbols[sClass].extends || sBaseClass; + sVisibility = externalSymbols[sClass].visibility || sVisibility; + debug("create doclet for external class " + sClass + " (extended by " + sExtendingClass + ")"); + } else { + warning("create missing class " + sClass + " (extended by " + sExtendingClass + ")"); } var oBaseClass = getOrCreateClass(sBaseClass, sClass); oClass = makeDoclet(sClass, [ "@extends " + sBaseClass, - "@class", + "@" + sKind, "@synthetic", - "@public" + sVisibility === "restricted" ? "@ui5-restricted" : "@" + sVisibility ]); oClass.__ui5.base = oBaseClass; oBaseClass.__ui5.derived = oBaseClass.__ui5.derived || []; @@ -786,12 +795,17 @@ function createInheritanceTree() { for (var j = 0; j < oClass.implements.length; j++) { var oInterface = lookup(oClass.implements[j]); if ( !oInterface ) { - warning("create missing interface " + oClass.implements[j]); + var sVisibility = "public"; + if ( externalSymbols[oClass.implements[j]] ) { + sVisibility = externalSymbols[oClass.implements[j]] || sVisibility; + debug("create doclet for external interface " + oClass.implements[j]); + } else { + warning("create missing interface " + oClass.implements[j]); + } oInterface = makeDoclet(oClass.implements[j], [ - "@extends Object", "@interface", "@synthetic", - "@public" + sVisibility === "restricted" ? "@ui5-restricted" : "@" + sVisibility ]); oInterface.__ui5.base = oObject; oObject.__ui5.derived = oObject.__ui5.derived || []; @@ -1697,7 +1711,7 @@ function TypeParser(defaultBuilder) { * - function(this:) // type of this * - function(new:) // constructor */ - var rLexer = /\s*(Array\.?<|Object\.?<|Set\.?<|Promise\.?<|function\(|\{|:|\(|\||\}|>|\)|,|\[\]|\*|\?|!|\.\.\.)|\s*((?:module:)?\w+(?:[\/.#~]\w+)*)|./g; + var rLexer = /\s*(Array\.?<|Object\.?<|Set\.?<|Promise\.?<|function\(|\{|:|\(|\||\}|\.?<|>|\)|,|\[\]|\*|\?|!|\.\.\.)|\s*((?:module:)?\w+(?:[\/.#~]\w+)*)|./g; var input, builder, @@ -1836,9 +1850,24 @@ function TypeParser(defaultBuilder) { } else { type = builder.simpleType(tokenStr); next('symbol'); - while ( token === '[]' ) { + // check for suffix operators: either 'type application' (generics) or 'array', but not both of them + if ( token === "<" || token === ".<" ) { next(); - type = builder.array(type); + var templateTypes = []; + while ( token !== ">" ) { + var templateType = parseType(); + templateTypes.push(templateType); + if ( token === ',' ) { + next(); + } + } + next(">"); + type = builder.typeApplication(type, templateTypes); + } else { + while ( token === '[]' ) { + next(); + type = builder.array(type); + } } } if ( nullable ) { @@ -1942,6 +1971,13 @@ TypeParser.ASTBuilder = { repeatable: function(type) { type.repeatable = true; return type; + }, + typeApplication: function(type, templateTypes) { + return { + type: 'typeApplication', + baseType: type, + templateTypes: templateTypes + }; } }; @@ -1972,6 +2008,7 @@ TypeParser.LinkBuilder.prototype = { array: function(componentType) { if ( componentType.needsParenthesis || componentType.simpleComponent === false ) { return { + simpleComponent: false, str: "Array.<" + this.safe(componentType) + ">" }; } @@ -1982,20 +2019,24 @@ TypeParser.LinkBuilder.prototype = { object: function(keyType, valueType) { if ( keyType.synthetic ) { return { + simpleComponent: false, str: "Object." + this.lt + this.safe(valueType) + this.gt }; } return { + simpleComponent: false, str: "Object." + this.lt + this.safe(keyType) + "," + this.safe(valueType) + this.gt }; }, set: function(elementType) { return { + simpleComponent: false, str: 'Set.' + this.lt + this.safe(elementType) + this.gt }; }, promise: function(fulfillmentType) { return { + simpleComponent: false, str: 'Promise.' + this.lt + this.safe(fulfillmentType) + this.gt }; }, @@ -2040,6 +2081,12 @@ TypeParser.LinkBuilder.prototype = { repeatable: function(type) { type.str = "..." + type.str; return type; + }, + typeApplication: function(type, templateTypes) { + return { + simpleComponent: false, + str: this.safe(type) + this.lt + templateTypes.map(function(type) { return this.safe(type); }, this).join(',') + this.gt + }; } }; @@ -2183,7 +2230,13 @@ function createAPIJSON(symbols, filename) { // sort only a copy(!) of the symbols, otherwise the SymbolSet lookup is broken symbols.slice(0).sort(sortByAlias).forEach(function(symbol) { if ( isFirstClassSymbol(symbol) && !symbol.synthetic ) { // dump a symbol if it as a class symbol and if it is not a synthetic symbol - api.symbols.push(createAPIJSON4Symbol(symbol, false)); + try { + var json = createAPIJSON4Symbol(symbol, false); + api.symbols.push(json); + } catch (e) { + error("failed to create api summary for " + symbol.name, e); + throw e; + } } }); @@ -2214,6 +2267,13 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { return true; } + // In some cases, JSDoc does not provide a basename in property symbol.name, but a partially qualified name + // this function reduces this to the base name + function basename(name) { + var p = name.lastIndexOf("."); + return p < 0 ? name : name.slice(p + 1); + } + function tag(name, value, omitEmpty) { if ( omitEmpty && !value ) { @@ -2766,7 +2826,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { tag(kind); attrib("name", symbol.longname); - attrib("basename", symbol.name); + attrib("basename", basename(symbol.name)); if ( symbol.__ui5.resource ) { attrib("resource", symbol.__ui5.resource); } @@ -2810,16 +2870,20 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { var skipMembers = false; var i, j, member, param; + var standardEnum = false; if ( kind === 'class' ) { - if ( symbol.__ui5.stereotype || hasSettings(symbol) ) { + if ( symbol.__ui5.stereotype || (symbol.__ui5.metadata && symbol.__ui5.metadata.metadataClass) || hasSettings(symbol) ) { tag("ui5-metadata"); if ( symbol.__ui5.stereotype ) { attrib("stereotype", symbol.__ui5.stereotype); } + if ( symbol.__ui5.metadata && symbol.__ui5.metadata.metadataClass ) { + attrib("metadataClass", symbol.__ui5.metadata.metadataClass); + } writeMetadata(symbol); @@ -2827,7 +2891,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { } - // IF @hideconstructor tag is present we omit the whole constructor + // if @hideconstructor tag is present we omit the whole constructor if ( !symbol.hideconstructor ) { tag("constructor"); @@ -2882,6 +2946,13 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { }); endCollection("properties"); } + } else if ( kind === 'enum' ) { + if ( symbol.__ui5.stereotype ) { + tag("ui5-metadata"); + attrib("stereotype", symbol.__ui5.stereotype); + standardEnum = symbol.__ui5.stereotype === 'enum'; + closeTag("ui5-metadata"); + } } else if ( kind === 'function' ) { methodSignature(symbol); } @@ -2898,6 +2969,9 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { attrib("module", member.__ui5.module); attrib("export", undefined, '', true); } + if ( kind === 'enum' && !standardEnum && member.__ui5.value !== undefined ) { + attrib("value", member.__ui5.value, undefined, /* raw = */true); + } attrib("visibility", visibility(member), 'public'); if ( member.scope === 'static' ) { attrib("static", true, false, /* raw = */true); @@ -3069,7 +3143,10 @@ function postProcessAPIJSON(api) { symbols.forEach(function(symbol) { // debug("check ", symbol.name, "against", defaultExport, "and", moduleNamePath); - if ( symbol.name === moduleNamePath ) { + if ( symbol.symbol.kind === "typedef" || symbol.symbol.kind === "interface" ) { + // type definitions and interfaces have no representation on module level + symbol.symbol.export = undefined; + } else if ( symbol.name === moduleNamePath ) { // symbol name is the same as the module namepath -> symbol is the default export symbol.symbol.export = ""; } else if ( symbol.name.lastIndexOf(moduleNamePath + ".", 0) === 0 ) { @@ -3091,7 +3168,7 @@ function postProcessAPIJSON(api) { || (symbol.symbol.methods && symbol.symbol.methods.length > 0) ) { error("could not identify export name of '" + symbol.name + "', contained in module '" + moduleName + "'"); } else { - debug("could not identify export name of namespace '" + symbol.name + "', contained in module '" + moduleName + "'"); + debug("could not identify export name of " + symbol.symbol.kind + " '" + symbol.name + "', contained in module '" + moduleName + "'"); } } }); @@ -3101,21 +3178,61 @@ function postProcessAPIJSON(api) { for ( n in modules ) { guessExports(n, modules[n]); } + + function findSymbol(name) { + if ( name == null || name === '' ) { + return null; + } + return symbols.find(function(candidate) { + return candidate.name === name; + }) || externalSymbols[name]; + } + + function findMetadataClass(symbol) { + while ( symbol ) { + if ( symbol["ui5-metadata"] && symbol["ui5-metadata"].metadataClass ) { + var metadataSymbol = findSymbol(symbol["ui5-metadata"].metadataClass); + if ( metadataSymbol != null && metadataSymbol.visibility === "public" ) { + return symbol["ui5-metadata"].metadataClass; + } + } + symbol = findSymbol(symbol.extends); + } + // return undefined + } + + symbols.forEach(function(symbol) { + if ( !symbol["ui5-metadata"] ) { + return; + } + if ( Array.isArray(symbol.methods) ) { + symbol.methods.forEach(function(method) { + if ( method.name === "getMetadata" && method.returnValue ) { + var metadataClass = findMetadataClass(symbol); + if ( metadataClass && metadataClass !== method.returnValue.type ) { + method.returnValue.type = metadataClass; + debug(" return type of " + symbol.name + (method.static ? "." : "#") + "getMetadata changed to '" + metadataClass + "'"); + } + } + }); + } + }); + } var builtinTypes = { - void:true, + "void":true, any:true, - boolean:true, - int: true, - float:true, + "boolean":true, + "int": true, + "float":true, array:true, - function:true, + "function":true, string:true, object:true, "*": true, number:true, - null:true, + "null":true, undefined:true, // builtin objects @@ -3129,22 +3246,45 @@ var builtinTypes = { Promise:true, ArrayBuffer:true, Uint8Array:true, - Blob:true, Error:true, TypeError:true, SyntaxError:true, - // DOM & Browser APIs + // Web APIs + Blob:true, Document:true, Element:true, Event:true, + File:true, HTMLElement: true, Node:true, Touch:true, TouchList:true, Window: true + }; +var typeNormalizer = (function() { + function TypeNormalizer() { + TypeParser.LinkBuilder.call(this, 'text', false); + } + TypeNormalizer.prototype = Object.create(TypeParser.LinkBuilder.prototype); + TypeNormalizer.prototype.simpleType = function(type) { + if ( type === 'map' ) { + return this.object( + this.simpleType('string'), + this.simpleType('any') + ); + } + if ( type === '*' ) { + type = 'any'; + } + return TypeParser.LinkBuilder.prototype.simpleType.call(this, type); + }; + + return new TypeNormalizer(); +}()); + function validateAPIJSON(api) { // create map of defined symbols (built-in types, dependency libraries, current library) @@ -3153,14 +3293,66 @@ function validateAPIJSON(api) { api.symbols.forEach(function(symbol) { defined[symbol.name] = symbol; }); } + var naming = Object.create(null); var missing = Object.create(null); + var rValidNames = /^[$A-Z_a-z][$0-9A-Z_a-z]*$/i; + var rValidModuleNames = /^[$A-Z_a-z][$\-\.0-9A-Z_a-z]*$/i; + + function checkName(name, hint) { + if ( !rValidNames.test(name) ) { + naming[name] = naming[name] || []; + naming[name].push(hint); + } + } + + function checkModuleName(name, hint) { + if ( !rValidModuleNames.test(name) ) { + naming[name] = naming[name] || []; + naming[name].push(hint); + } + } + + function checkCompoundName(name, hint) { + + if ( name.startsWith("module:") ) { + var segments = name.slice("module:".length).split("/"); + + // split last segment into a module name part and a symbol name part + var p = segments[segments.length - 1].search(/[.~#]/); + if ( p >= 0 ) { + name = segments[segments.length - 1].slice(p + 1); + segments[segments.length - 1] = segments[segments.length - 1].slice(0, p); + } + + // check all module name parts + segments.forEach(function(segment) { + checkModuleName(segment, "path segment of " + hint); + }); + + if ( p < 0 ) { + // module name only, no export name to check + return; + } + } + + name.split(/[.~#]/).forEach(function(segment) { + checkName(segment, "name segment of " + hint); + }); + } + function reportError(type, usage) { missing[type] = missing[type] || []; missing[type].push(usage); } - function check(type, hint) { + function checkSimpleType(typeName, hint) { + if ( !defined[typeName] ) { + reportError(typeName, hint); + } + } + + function checkType(type, hint) { function _check(type) { if ( type == null ) { @@ -3168,9 +3360,7 @@ function validateAPIJSON(api) { } switch (type.type) { case 'simpleType': - if ( !defined[type.name] ) { - reportError(type.name, hint); - } + checkSimpleType(type.name, hint); break; case 'array': _check(type.component); @@ -3199,21 +3389,31 @@ function validateAPIJSON(api) { case 'union': type.types.forEach(_check); break; + case 'typeApplication': + _check(type.baseType); + type.templateTypes.forEach(_check); + // TODO check number of templateTypes against declaration of baseType + // requires JSDoc support of @template tag, which is currently missing + break; default: break; } } try { - // console.log("check", type); - var ast = typeParser.parse(type); + // debug("normalize", type); + type.type = typeParser.parse(type.type, typeNormalizer).str; + // debug("check", type); + var ast = typeParser.parse(type.type); _check(ast); } catch (e) { - reportError(type, "failed to parse type of " + hint); + error(e); + reportError(type.type, "failed to parse type of " + hint); } } function checkParam(param, prefix, hint) { - check(param.type, "param " + prefix + param.name + " of " + hint); + checkName(param.name, "name of param " + prefix + param.name + " of " + hint); + checkType(param, "param " + prefix + param.name + " of " + hint); if ( param.parameterProperties ) { Object.keys(param.parameterProperties).forEach(function(sub) { checkParam(param.parameterProperties[sub], prefix + param.name + ".", hint); @@ -3221,9 +3421,9 @@ function validateAPIJSON(api) { } } - function checkMethod(method, hint) { + function checkMethodSignature(method, hint) { if ( method.returnValue ) { - check(method.returnValue.type, "return value of " + hint); + checkType(method.returnValue, "return value of " + hint); } if ( method.parameters ) { method.parameters.forEach(function(param) { @@ -3232,7 +3432,7 @@ function validateAPIJSON(api) { } if ( method.throws ) { method.throws.forEach(function(ex) { - check(ex.type, "exception of " + hint); + checkType(ex, "exception of " + hint); }); } } @@ -3240,49 +3440,93 @@ function validateAPIJSON(api) { function checkClassAgainstInterface(symbol, oIntfAPI) { if ( oIntfAPI.methods ) { oIntfAPI.methods.forEach(function(intfMethod) { - // ignore optional methods - if ( intfMethod.optional ) { - return; - } // search for method implementation var implMethod = symbol.methods.find(function(candidateMethod) { return candidateMethod.name === intfMethod.name && !candidateMethod.static; - }) + }); if ( !implMethod ) { - reportError(oIntfAPI.name, "implementation of " + intfMethod.name + " missing in " + symbol.name); + if ( !intfMethod.optional ) { + reportError(oIntfAPI.name, "implementation of " + intfMethod.name + " missing in " + symbol.name); + } + } else { + if ( intfMethod.parameters ) { + intfMethod.parameters.forEach(function(intfParam, idx) { + var implParam = implMethod.parameters && implMethod.parameters[idx]; + if ( !implParam ) { + if ( !intfParam.optional ) { + reportError(oIntfAPI.name, "parameter " + intfParam.name + " missing in implementation of " + symbol.name + "#" + intfMethod.name); + } + } else { + if ( implParam.type !== intfParam.type ) { + reportError(oIntfAPI.name, "type of parameter " + intfParam.name + " of interface method differs from type in implementation " + symbol.name + "#" + intfMethod.name); + } + // TODO check nested properties + } + }); + } + if ( intfMethod.returnValue != null && implMethod.returnValue == null ) { + reportError(oIntfAPI.name, "return value of interface method missing in implementation " + symbol.name + "#" + intfMethod); + } else if ( intfMethod.returnValue == null && implMethod.returnValue != null ) { + reportError(oIntfAPI.name, "while interface method is void, implementation " + symbol.name + "#" + intfMethod.name + " returns a value"); + } else if ( intfMethod.returnValue != null && implMethod.returnValue != null ) { + if ( intfMethod.returnValue.type !== implMethod.returnValue.type ) { + reportError(oIntfAPI.name, "return type of interface method differs from return type of implementation " + symbol.name + "#" + intfMethod.name); + } + } + } + }); + } + } + + function checkEnum(symbol) { + if ( symbol["ui5-metamodel"] && !(symbol["ui5-metadata"] && symbol["ui5-metadata"].stereotype === "enum") ) { + reportError(symbol.name, "enum is metamodel relevant but keys and values differ"); + } + checkCompoundName(symbol.name, "name of " + symbol.name); + if ( symbol.properties ) { + symbol.properties.forEach(function(prop) { + checkName(prop.name, "name of " + symbol.name + "." + prop.name); + if ( prop.type ) { + checkType(prop, "type of " + symbol.name + "." + prop.name); } - // TODO check parameters }); } } function checkClass(symbol) { + checkCompoundName(symbol.name, "name of " + symbol.name); if ( symbol.extends ) { - check(symbol.extends, "base class of " + symbol.name); + checkSimpleType(symbol.extends, "base class of " + symbol.name); } if ( symbol.implements ) { symbol.implements.forEach(function(intf) { - check(intf, "interface of " + symbol.name); + checkSimpleType(intf, "interface of " + symbol.name); var oIntfAPI = defined[intf]; if ( oIntfAPI ) { checkClassAgainstInterface(symbol, oIntfAPI); } }); } + if ( Object.prototype.hasOwnProperty.call(symbol, "constructor") ) { + checkMethodSignature(symbol.constructor, symbol.name + ".constructor"); + } if ( symbol.properties ) { symbol.properties.forEach(function(prop) { + checkName(prop.name, "name of " + symbol.name + "." + prop.name); if ( prop.type ) { - check(prop.type, "type of " + symbol.name + "." + prop.name); + checkType(prop, "type of " + symbol.name + "." + prop.name); } }); } if ( symbol.methods ) { symbol.methods.forEach(function(method) { - checkMethod(method, symbol.name + "." + method.name); + checkName(method.name, "name of " + symbol.name + "." + method.name); + checkMethodSignature(method, symbol.name + "." + method.name); }); } if ( symbol.events ) { symbol.events.forEach(function(event) { + checkName(event.name, "name of " + symbol.name + "." + event.name); if ( event.parameters ) { event.parameters.forEach(function(param) { checkParam(param, '', symbol.name + "." + event.name); @@ -3294,20 +3538,38 @@ function validateAPIJSON(api) { api.symbols.forEach(function(symbol) { if ( symbol.kind === 'function' ) { - checkMethod(symbol, symbol.name); + checkCompoundName(symbol.name, "name of " + symbol.name); + checkMethodSignature(symbol, symbol.name); + } else if ( symbol.kind === 'enum' ) { + checkEnum(symbol); } else { checkClass(symbol); } }); - for (var type in missing) { - if ( Array.isArray(missing[type]) ) { - error(type); - missing[type].forEach(function(usage) { - error(" " + usage); - }); - } + if ( Object.keys(missing).length > 0 || Object.keys(naming).length > 0 ) { + error("API validation errors:"); + + Object.keys(missing).forEach(function(type) { + if ( Array.isArray(missing[type]) ) { + error("type '" + type + "'"); + missing[type].forEach(function(usage) { + error(" " + usage); + }); + } + }); + Object.keys(naming).forEach(function(name) { + if ( Array.isArray(naming[name]) ) { + error("invalid name '" + name + "'"); + naming[name].forEach(function(usage) { + error(" " + usage); + }); + } + }); + } else { + info("API validation succeeded."); } + } //---- add on: API XML ----------------------------------------------------------------- @@ -4353,3 +4615,4 @@ function makeExample(example) { /* ---- exports ---- */ exports.publish = publish; +