diff --git a/lib/processors/jsdoc/lib/createIndexFiles.js b/lib/processors/jsdoc/lib/createIndexFiles.js index 94c57dcc8..262a267ae 100644 --- a/lib/processors/jsdoc/lib/createIndexFiles.js +++ b/lib/processors/jsdoc/lib/createIndexFiles.js @@ -39,12 +39,12 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile log.info("Using custom fs"); } if (returnOutputFiles) { - log.info("Returning output files instead of writing to fs.") + log.info("Returning output files instead of writing to fs."); } log.info(""); // Deprecated, Experimental and Since collections - let oListCollection = { + const oListCollection = { deprecated: { noVersion: { apis: [] @@ -63,13 +63,11 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile fs.readFile(file, 'utf8', function (err, data) { if (err) { reject(err); - } else { + } else if (data.trim() === "") { // Handle empty files scenario - if (data.trim() === "") { - resolve({}); - } else { - resolve(JSON.parse(String(data))); - } + resolve({}); + } else { + resolve(JSON.parse(String(data))); } }); }); @@ -102,20 +100,20 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile * Returns a promise that resolves with an array of symbols. */ function createSymbolSummaryForLib(lib) { - let file = path.join(unpackedTestresourcesRoot, lib.replace(/\./g, "/"), "designtime/api.json"); + const file = path.join(unpackedTestresourcesRoot, lib.replace(/\./g, "/"), "designtime/api.json"); return readJSONFile(file).then(function (apijson) { if (!apijson.hasOwnProperty("symbols") || !Array.isArray(apijson.symbols)) { // Ignore libraries with invalid api.json content like empty object or non-array "symbols" property. return []; } - return apijson.symbols.map(symbol => { - let oReturn = { + return apijson.symbols.map((symbol) => { + const oReturn = { name: symbol.name, kind: symbol.kind, visibility: symbol.visibility, - extends: symbol.extends, - implements: symbol.implements, + "extends": symbol.extends, + "implements": symbol.implements, lib: lib }; // We add deprecated member only when the control is deprecated to keep file size at check @@ -132,7 +130,7 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile collectLists(symbol); return oReturn; }); - }) + }); } /* @@ -146,7 +144,7 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile const sText = oDataType !== "since" ? oEntityObject[oDataType].text : oEntityObject.description; const oData = { control: sSymbolName, - text: sText || undefined, + text: sText || undefined, type: sObjectType, "static": !!oEntityObject.static, visibility: oEntityObject.visibility @@ -159,7 +157,7 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile if (sSince && sSince !== "undefined" /* Sometimes sSince comes as string "undefined" */) { // take only major and minor versions - let sVersion = sSince.split(".").slice(0, 2).join("."); + const sVersion = sSince.split(".").slice(0, 2).join("."); oData.since = sSince; @@ -190,7 +188,7 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile } // Methods - oSymbol.methods && oSymbol.methods.forEach(oMethod => { + oSymbol.methods?.forEach((oMethod) => { if (oMethod.deprecated) { addData("deprecated", oMethod, "methods", oSymbol.name); } @@ -205,7 +203,7 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile }); // Events - oSymbol.events && oSymbol.events.forEach(oEvent => { + oSymbol.events?.forEach((oEvent) => { if (oEvent.deprecated) { addData("deprecated", oEvent, "events", oSymbol.name); } @@ -229,24 +227,24 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile } function expandHierarchyInfo(symbols) { - let byName = new Map(); - symbols.forEach(symbol => { + const byName = new Map(); + symbols.forEach((symbol) => { byName.set(symbol.name, symbol); }); - symbols.forEach(symbol => { - let parent = symbol.extends && byName.get(symbol.extends); + symbols.forEach((symbol) => { + const parent = symbol.extends && byName.get(symbol.extends); if (parent) { - parent.extendedBy = parent.extendedBy ||  []; + parent.extendedBy = parent.extendedBy || []; parent.extendedBy.push({ name: symbol.name, visibility: symbol.visibility }); } if (symbol.implements) { - symbol.implements.forEach(intfName => { - let intf = byName.get(intfName); + symbol.implements.forEach((intfName) => { + const intf = byName.get(intfName); if (intf) { - intf.implementedBy = intf.implementedBy ||  []; + intf.implementedBy = intf.implementedBy || []; intf.implementedBy.push({ name: symbol.name, visibility: symbol.visibility @@ -259,23 +257,23 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile } function convertListToTree(symbols) { - let aTree = []; + const aTree = []; // Filter out excluded libraries symbols = symbols.filter(({lib}) => ["sap.ui.documentation"].indexOf(lib) === -1); // Create treeName and displayName - symbols.forEach(oSymbol => { + symbols.forEach((oSymbol) => { oSymbol.treeName = oSymbol.name.replace(/^module:/, "").replace(/\//g, "."); oSymbol.displayName = oSymbol.treeName.split(".").pop(); }); // Create missing - virtual namespaces - symbols.forEach(oSymbol => { + symbols.forEach((oSymbol) => { oSymbol.treeName.split(".").forEach((sPart, i, a) => { - let sName = a.slice(0, (i + 1)).join("."); + const sName = a.slice(0, (i + 1)).join("."); - if (!symbols.find(o => o.treeName === sName)) { + if (!symbols.find((o) => o.treeName === sName)) { symbols.push({ name: sName, treeName: sName, @@ -289,13 +287,12 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile }); // Discover parents - symbols.forEach(oSymbol => { - let aParent = oSymbol.treeName.split("."), - sParent; + symbols.forEach((oSymbol) => { + const aParent = oSymbol.treeName.split("."); // Extract parent name aParent.pop(); - sParent = aParent.join("."); + const sParent = aParent.join("."); // Mark parent if (symbols.find(({treeName}) => treeName === sParent)) { @@ -305,20 +302,20 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile // Sort the list before building the tree symbols.sort((a, b) => { - let sA = a.treeName.toUpperCase(), + const sA = a.treeName.toUpperCase(), sB = b.treeName.toUpperCase(); - if (sA < sB) return -1; - if (sA > sB) return 1; + if (sA < sB) {return -1;} + if (sA > sB) {return 1;} return 0; }); // Build tree - symbols.forEach(oSymbol => { + symbols.forEach((oSymbol) => { if (oSymbol.parent) { - let oParent = symbols.find(({treeName}) => treeName === oSymbol.parent); + const oParent = symbols.find(({treeName}) => treeName === oSymbol.parent); - if (!oParent.nodes) oParent.nodes = []; + if (!oParent.nodes) {oParent.nodes = [];} oParent.nodes.push(oSymbol); } else { aTree.push(oSymbol); @@ -327,13 +324,13 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile // Custom sort first level tree items - "sap" namespace should be on top aTree.sort((a, b) => { - let sA = a.displayName.toUpperCase(), + const sA = a.displayName.toUpperCase(), sB = b.displayName.toUpperCase(); - if (sA === "SAP") return -1; - if (sB === "SAP") return 1; - if (sA < sB) return -1; - if (sA > sB) return 1; + if (sA === "SAP") {return -1;} + if (sB === "SAP") {return 1;} + if (sA < sB) {return -1;} + if (sA > sB) {return 1;} return 0; }); @@ -349,10 +346,10 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile delete oSymbol.treeName; delete oSymbol.parent; if (oSymbol.nodes) { - oSymbol.nodes.forEach(o => cleanTree(o)); + oSymbol.nodes.forEach((o) => cleanTree(o)); } } - aTree.forEach(o => cleanTree(o)); + aTree.forEach((o) => cleanTree(o)); return aTree; } @@ -401,7 +398,7 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile 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); + oSymbol.bAllContentDeprecated = !!oSymbol.nodes && oSymbol.nodes.every((node) => node.bAllContentDeprecated); } } @@ -413,9 +410,9 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile function propagateFlags(oSymbol, oFlags) { Object.assign(oSymbol, oFlags); if (oSymbol.nodes) { - oSymbol.nodes.forEach(node => { + oSymbol.nodes.forEach((node) => { propagateFlags(node, oFlags); - }) + }); } } @@ -424,11 +421,11 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile const filesToReturn = {}; var p = readJSONFile(versionInfoFile) - .then(versionInfo => { + .then((versionInfo) => { version = versionInfo.version; return Promise.all( versionInfo.libraries.map( - lib => createSymbolSummaryForLib(lib.name).catch(err => { + (lib) => createSymbolSummaryForLib(lib.name).catch((err) => { // ignore 'file not found' errors as some libs don't have an api.json (themes, server libs) if (err.code === 'ENOENT') { return []; @@ -441,8 +438,8 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile .then(deepMerge) .then(expandHierarchyInfo) .then(convertListToTree) - .then(symbols => { - let result = { + .then((symbols) => { + const result = { "$schema-ref": "http://schemas.sap.com/sapui5/designtime/api-index.json/1.0", version: version, library: "*", @@ -456,13 +453,13 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile }) .then(() => { /* Lists - modify and cleanup */ - let sortList = function (oList) { + const sortList = function (oList) { /* Sorting since records */ - let aKeys = Object.keys(oList), + const aKeys = Object.keys(oList), oSorted = {}; aKeys.sort((a, b) => { - let aA = a.split("."), + const aA = a.split("."), aB = b.split("."); if (a === "noVersion") { @@ -478,7 +475,7 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile b = [aB[0], ('0' + aB[1]).slice(-2)].join(""); // Sort descending - return parseInt(b, 10) - parseInt(a, 10); + return parseInt(b) - parseInt(a); }); aKeys.forEach((sKey) => { @@ -529,8 +526,8 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile ]); } }) - .catch(err => { - log.error("**** failed to create API index for libraries:", err) + .catch((err) => { + log.error("**** failed to create API index for libraries:", err); throw err; }); diff --git a/lib/processors/jsdoc/lib/transformApiJson.js b/lib/processors/jsdoc/lib/transformApiJson.js index 1969ed1fb..466ccba2a 100644 --- a/lib/processors/jsdoc/lib/transformApiJson.js +++ b/lib/processors/jsdoc/lib/transformApiJson.js @@ -1,15 +1,14 @@ -/* +/** * Node script to preprocess api.json files for use in the UI5 SDKs. * * (c) Copyright 2009-2024 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ -/* eslint-disable */ - "use strict"; const cheerio = require("cheerio"); const path = require('path'); +const {TypeParser} = require("./ui5/template/utils/typeParser"); const log = (function() { try { return require("@ui5/logger").getLogger("builder:processors:jsdoc:transformApiJson"); @@ -36,13 +35,9 @@ function normalizeToUI5GlobalNotation(sModuleName){ return sModuleName.replace(/\//g, "."); } -function fnCreateTypesArr(sTypes) { - return sTypes.split("|").map(function (sType) { - return { value: sType } - }); -} +const typeParser = new TypeParser(); -/* +/** * Transforms the api.json as created by the JSDoc build into a pre-processed api.json file suitable for the SDK. * * The pre-processing includes formatting of type references, rewriting of links and other time consuming calculations. @@ -69,72 +64,208 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, log.info("Using custom fs."); } if (returnOutputFiles) { - log.info("Returning output files instead of writing to fs.") + log.info("Returning output files instead of writing to fs."); } log.info(""); function resolveTypeParameters(func) { - if ( func.typeParameters ) { - const replacements = Object.create(null); - let regex; - const _resolve = (str) => - str && regex ? str.replace(regex, (_,prefix,token,suffix) => prefix + replacements[token] + suffix) : str; - - func.typeParameters.forEach((typeParam) => { - typeParam.type = _resolve(typeParam.type); - replacements[typeParam.name] = typeParam.type || "any"; - regex = new RegExp("(^|\\W)(" + Object.keys(replacements).join("|") + ")(\\W|$)"); - typeParam.default = _resolve(typeParam.default); - }); - - function _resolveParamProperties(param) { - if ( param.parameterProperties ) { - Object.keys(param.parameterProperties).forEach((key) => { - const prop = param.parameterProperties[key]; - prop.type = _resolve(prop.type); - _resolveParamProperties(prop); - }); - } - } + if ( !func.typeParameters ) { + return func; + } + const replacements = Object.create(null); + let regex; + const _resolve = (str) => + (str && regex ? str.replace(regex, (_,prefix,token,suffix) => prefix + replacements[token] + suffix) : str); + + func.typeParameters.forEach((typeParam) => { + typeParam.type = _resolve(typeParam.type); + replacements[typeParam.name] = typeParam.type || "any"; + regex = new RegExp("(^|\\W)(" + Object.keys(replacements).join("|") + ")(\\W|$)"); + typeParam.default = _resolve(typeParam.default); + }); - if ( func.returnValue ) { - func.returnValue.type = _resolve(func.returnValue.type); - } - if ( func.parameters ) { - func.parameters.forEach((param) => { - param.type = _resolve(param.type); - _resolveParamProperties(param); - }); - } - if ( func.throws ) { - func.throws.forEach((ex) => { - ex.type = _resolve(ex.type); + function _resolveParamProperties(param) { + if ( param.parameterProperties ) { + Object.keys(param.parameterProperties).forEach((key) => { + const prop = param.parameterProperties[key]; + prop.type = _resolve(prop.type); + _resolveParamProperties(prop); }); } - console.log(func); + } + + if ( func.returnValue ) { + func.returnValue.type = _resolve(func.returnValue.type); + } + if ( func.parameters ) { + func.parameters.forEach((param) => { + param.type = _resolve(param.type); + _resolveParamProperties(param); + }); + } + if ( func.throws ) { + func.throws.forEach((ex) => { + ex.type = _resolve(ex.type); + }); } return func; } + function isUI5Type(sType) { + return !isBuiltInType(sType) && possibleUI5Symbol(sType); + } + /** - * Transforms api.json file - * @param {object} oChainObject chain object + * Returns an object with the parsed custom-type information, namely: + * - types: array of the parsed UI5 types inside the given complex type + * - template: the template string with placeholders for the parsed UI5 types + * + * Examples: + * + * - parseUI5Types("sap.ui.core.ID | sap.ui.core.Control") returns + * { + * template: "${0} | ${1}", + * UI5Types: ["sap.ui.core.ID", "sap.ui.core.Control"] + * } + * + * - parseUI5Types("Array") returns + * { + * template: "Array<${0}>", + * UI5Types: ["sap.ui.core.Control"] + * } + * + * - parseUI5Types("Array") returns + * { + * template: "Array<${0} | string>", // built-in types remain unchanged in the template + * UI5Types: ["sap.ui.core.ID"] + * } + * + * - parseUI5Types("Object") returns + * { + * template: "Object", // built-in types remain unchanged in the template + * UI5Types: ["sap.ui.core.Control"] + * } + * + * - parseUI5Types("string") returns + * { + * template: "string" // skip the types array if empty + * } + * + * - parseUI5Types("sap.ui.core.Control") returns + * { + * UI5Types: ["sap.ui.core.Control"] // skip template if its value is "${0}" (default value) + * } + * @param {string} sComplexType + * @returns {{template=: string, UI5Types=: string[]}} */ - let transformApiJson = function (oChainObject) { - function isBuiltInType(type) { - return formatters._baseTypes.indexOf(type) >= 0; + function parseUI5Types(sComplexType) { + let oParsed; + try { + oParsed = typeParser.parseSimpleTypes(sComplexType, isUI5Type); + } catch (e) { + log.error("Error parsing type: " + sComplexType); + log.error(e); + oParsed = { template: sComplexType }; } - /** - * Heuristically determining if there is a possibility the given input string - * to be a UI5 symbol - * @param {string} sName - * @returns {boolean} - */ - function possibleUI5Symbol(sName) { - return /^[a-zA-Z][a-zA-Z0-9.]*[a-zA-Z0-9]$/.test(sName); + const result = {}; + + if (oParsed.template !== '${0}') { // default is '${0}', so omit if not required + result.template = oParsed.template; + } + + if (oParsed.simpleTypes?.length) { // it can be empty if none of the included simple types satisfied the filter function + result.UI5Types = oParsed.simpleTypes; + } + + return result; + } + + function includesIgnoreCase(array, string) { + return array.some((item) => item.toLowerCase() === string.toLowerCase()); + } + + /** + * Check if a type is a built-in type, handling both simple + * and compound types gracefully. + * + * @param {string} type A type to check + * @returns {boolean} true if the type is built-in, otherwise false + */ + function isBuiltInType(type) { + const builtInTypes = formatters._baseTypes; + + // Early return if the type is directly in baseTypes + if (includesIgnoreCase(builtInTypes, type)) { + return true; + } + + // Check if the type is a union type + if (type.includes("|")) { + const unionParts = type.split("|").map((part) => part.trim()); + return unionParts.every((part) => isBuiltInType(part)); + } + + // Handle array notation directly + if (type.endsWith("[]")) { + return builtInTypes.includes(type.slice(0, -2)); + } + + // Predefined regex patterns for reuse + const arrayRegex = /Array<(.+)>/; + const arrayDotRegex = /Array\.<(.+)>/; + const objectRegex = /Object<([^,]+),([^>]+)>/; + const objectDotRegex = /Object\.<([^,]+),([^>]+)>/; + + // Check if the type is a generic Array type + const arrayMatch = arrayRegex.exec(type); + if (arrayMatch) { + const innerType = arrayMatch[1]; + return isBuiltInType(innerType); + } + + const arrayDotMatch = arrayDotRegex.exec(type); + if (arrayDotMatch) { + const innerType = arrayDotMatch[1]; + return isBuiltInType(innerType); } + // Check if the type is a generic Object type + const objectMatch = objectRegex.exec(type); + if (objectMatch) { + const innerTypes = [objectMatch[1], objectMatch[2]].map((t) => t.trim()); + return innerTypes.every((innerType) => isBuiltInType(innerType)); + } + + const objectDotMatch = objectDotRegex.exec(type); + if (objectDotMatch) { + const innerTypes = [objectDotMatch[1], objectDotMatch[2]].map((t) => t.trim()); + return innerTypes.every((innerType) => isBuiltInType(innerType)); + } + + // Fallback case: if none of the above matched, return false + return false; + } + + /** + * Heuristically determining if there is a possibility the given input string + * to be a UI5 symbol + * Examples of UI5 symbols: + * -"sap.ui.core.date.UI5Date" + * -"module:sap/ui/core/date/UI5Date" + * @param {string} sName + * @returns {boolean} + */ + function possibleUI5Symbol(sName) { + const ui5SymbolRegex = /^(module:)?sap[/.][a-zA-Z][a-zA-Z0-9/.$]*[a-zA-Z0-9]$/; + return ui5SymbolRegex.test(sName); + } + + /** + * Transforms api.json file + * @param {object} oChainObject chain object + */ + const transformApiJson = function (oChainObject) { // Function is a copy from: LibraryInfo.js => LibraryInfo.prototype._getActualComponent => "match" inline method function matchComponent(sModuleName, sPattern) { sModuleName = sModuleName.toLowerCase(); @@ -153,18 +284,18 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, */ function preProcessSymbols(symbols) { // Create treeName and modify module names - symbols.forEach(oSymbol => { - let sModuleClearName = oSymbol.name.replace(/^module:/, ""); + symbols.forEach((oSymbol) => { + const sModuleClearName = oSymbol.name.replace(/^module:/, ""); oSymbol.displayName = sModuleClearName; oSymbol.treeName = normalizeToUI5GlobalNotation(sModuleClearName); }); // Create missing - virtual namespaces - symbols.forEach(oSymbol => { + symbols.forEach((oSymbol) => { oSymbol.treeName.split(".").forEach((sPart, i, a) => { - let sName = a.slice(0, (i + 1)).join("."); + const sName = a.slice(0, (i + 1)).join("."); - if (!symbols.find(o => o.treeName === sName)) { + if (!symbols.find((o) => o.treeName === sName)) { symbols.push({ name: sName, displayName: sName, @@ -177,13 +308,12 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, }); // Discover parents - symbols.forEach(oSymbol => { - let aParent = oSymbol.treeName.split("."), - sParent; + symbols.forEach((oSymbol) => { + const aParent = oSymbol.treeName.split("."); // Extract parent name aParent.pop(); - sParent = aParent.join("."); + const sParent = aParent.join("."); // Mark parent if (symbols.find(({treeName}) => treeName === sParent)) { @@ -192,14 +322,13 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, }); // Attach children info - symbols.forEach(oSymbol => { + symbols.forEach((oSymbol) => { if (oSymbol.parent) { - let oParent = symbols.find(({treeName}) => treeName === oSymbol.parent), - oNode; + const oParent = symbols.find(({treeName}) => treeName === oSymbol.parent); - if (!oParent.nodes) oParent.nodes = []; + if (!oParent.nodes) {oParent.nodes = [];} - oNode = { + const oNode = { name: oSymbol.displayName, description: formatters._preProcessLinksInTextBlock( extractFirstSentence(oSymbol.description) @@ -216,14 +345,14 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, }); // Clean list - keep file size down - symbols.forEach(o => { + symbols.forEach((o) => { delete o.treeName; delete o.parent; }); } // Transform to object - let oData = oChainObject.fileData; + const oData = oChainObject.fileData; // Attach default component for the library if available if (oChainObject.defaultComponent) { @@ -246,7 +375,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Attach symbol specific component if available (special cases) // Note: Last hit wins as there may be a more specific component pattern if (oChainObject.customSymbolComponents) { - Object.keys(oChainObject.customSymbolComponents).forEach(sComponent => { + Object.keys(oChainObject.customSymbolComponents).forEach((sComponent) => { if (matchComponent(normalizeToUI5GlobalNotation(oSymbol.displayName), sComponent)) { oSymbol.component = oChainObject.customSymbolComponents[sComponent]; } @@ -268,7 +397,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Constructor if (oSymbol.constructor) { - let oConstructor = resolveTypeParameters(oSymbol.constructor); + const oConstructor = resolveTypeParameters(oSymbol.constructor); // Description if (oConstructor.description) { @@ -300,21 +429,13 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (oConstructor.parameters) { oConstructor.parameters = methods.buildConstructorParameters(oConstructor.parameters); - let aParameters = oConstructor.parameters; - aParameters.forEach(oParameter => { + const aParameters = oConstructor.parameters; + aParameters.forEach((oParameter) => { // Types oParameter.types = []; if (oParameter.type) { - let aTypes = oParameter.type.split("|"); - - for (let i = 0; i < aTypes.length; i++) { - oParameter.types.push({ - name: aTypes[i], - linkEnabled: !isBuiltInType(aTypes[i]) - }); - } - + oParameter.typeInfo = parseUI5Types(oParameter.type); // Keep file size in check delete oParameter.type; } @@ -327,12 +448,12 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oParameter.description = formatters.formatDescription(oParameter.description); } - }) + }); } // Throws if (oConstructor.throws) { - oConstructor.throws.forEach(oThrows => { + oConstructor.throws.forEach((oThrows) => { // Description if (oThrows.description) { @@ -399,17 +520,9 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Type if (oSymbol.kind !== "enum") { // enum properties don't have an own type if (oProperty.type) { - oProperty.types = oProperty.type.split("|").map((sType) => { - const oType = { - value: sType - }; - // Link Enabled - if (!isBuiltInType(oType.value) && possibleUI5Symbol(oType.value)) { - oType.linkEnabled = true; - oType.href = "api/" + oType.value.replace("[]", ""); - } - return oType; - }); + oProperty.typeInfo = parseUI5Types(oProperty.type); + // Keep file size in check + delete oProperty.type; } } @@ -425,7 +538,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // UI5 Metadata if (oSymbol["ui5-metadata"]) { - let oMeta = oSymbol["ui5-metadata"]; + const oMeta = oSymbol["ui5-metadata"]; // Properties if (oMeta.properties) { @@ -447,17 +560,14 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Type if (oProperty.type) { - oProperty.types = fnCreateTypesArr(oProperty.type); + oProperty.typeInfo = parseUI5Types(oProperty.type); + // Keep file size in check + delete oProperty.type; } // Description oProperty.description = formatters.formatDescriptionSince(oProperty.description, oProperty.since); - // Link Enabled - if (!isBuiltInType(oProperty.type)) { - oProperty.linkEnabled = true; - } - // Default value oProperty.defaultValue = formatters.formatDefaultValue(oProperty.defaultValue); @@ -540,29 +650,31 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // 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; - } + oMeta.events.forEach((oEvent) => { + const aParams = oEvent.parameters; + + if (aParams) { + Object.keys(aParams).forEach((sParam) => { + const sSince = aParams[sParam].since; + const oDeprecated = aParams[sParam].deprecated; + const oEvtInSymbol = oSymbol.events.find((e) => e.name === oEvent.name); + const 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; + if (oDeprecated) { + oParamInSymbol.deprecated = oDeprecated; + } } - } - }) + }); + } }); // We don't need event's data from the UI5-metadata for now. Keep file size in check @@ -571,11 +683,12 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Special Settings if (oMeta.specialSettings) { - oMeta.specialSettings.forEach(oSetting => { + oMeta.specialSettings.forEach((oSetting) => { // Link Enabled - if (!isBuiltInType(oSetting.type)) { - oSetting.linkEnabled = true; + if (oSetting.type) { + oSetting.typeInfo = parseUI5Types(oSetting.type); + delete oSetting.type; // Keep file size in check } // Description @@ -591,7 +704,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Annotations if (oMeta.annotations) { - oMeta.annotations.forEach(oAnnotation => { + oMeta.annotations.forEach((oAnnotation) => { // Description oAnnotation.description = formatters.formatAnnotationDescription(oAnnotation.description, @@ -617,7 +730,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Pre-process events methods.buildEventsModel(oSymbol.events); - oSymbol.events.forEach(oEvent => { + oSymbol.events.forEach((oEvent) => { // Description if (oEvent.description) { @@ -632,11 +745,11 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Parameters if (oEvent.parameters && Array.isArray(oEvent.parameters)) { - oEvent.parameters.forEach(oParameter => { + oEvent.parameters.forEach((oParameter) => { - // Link Enabled - if (!isBuiltInType(oParameter.type)) { - oParameter.linkEnabled = true; + if (oParameter.type) { + oParameter.typeInfo = parseUI5Types(oParameter.type); + delete oParameter.type; // Keep file size in check } // Description @@ -662,7 +775,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Pre-process methods methods.buildMethodsModel(oSymbol.methods); - oSymbol.methods.forEach(oMethod => { + oSymbol.methods.forEach((oMethod) => { // Name and link if (oMethod.name) { @@ -681,7 +794,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, } // Examples - oMethod.examples && oMethod.examples.forEach(oExample => { + oMethod.examples?.forEach((oExample) => { oExample = formatters.formatExample(oExample.caption, oExample.text); }); @@ -696,20 +809,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Parameters if (oMethod.parameters) { - oMethod.parameters.forEach(oParameter => { - - // Types - if (oParameter.types) { - oParameter.types.forEach(oType => { - - // Link Enabled - if (!isBuiltInType(oType.value) && possibleUI5Symbol(oType.value)) { - oType.linkEnabled = true; - oType.href = "api/" + oType.value.replace("[]", ""); - } - - }); - } + oMethod.parameters?.forEach((oParameter) => { // Default value oParameter.defaultValue = formatters.formatDefaultValue(oParameter.defaultValue); @@ -731,24 +831,11 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Description oMethod.returnValue.description = formatters.formatDescription(oMethod.returnValue.description); - // Types - if (oMethod.returnValue.types) { - oMethod.returnValue.types.forEach(oType => { - - // Link Enabled - if (!isBuiltInType(oType.value) && possibleUI5Symbol(oType.value)) { - oType.href = "api/" + oType.value.replace("[]", ""); - oType.linkEnabled = true; - } - - }); - } - } // Throws if (oMethod.throws) { - oMethod.throws.forEach(oThrows => { + oMethod.throws.forEach((oThrows) => { // Description if (oThrows.description) { @@ -832,11 +919,11 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, } // otherwise, it names a directory that has to be scanned for the files - return new Promise(oResolve => { + return new Promise((oResolve) => { fs.readdir(vDependencyAPIFiles, function (oError, aItems) { if (!oError && aItems && aItems.length) { - let aFiles = []; - aItems.forEach(sItem => { + const aFiles = []; + aItems.forEach((sItem) => { aFiles.push(path.join(vDependencyAPIFiles, sItem)); }); oChainObject.aDependentLibraryFiles = aFiles; @@ -850,18 +937,18 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (!oChainObject.aDependentLibraryFiles) { return oChainObject; } - let aPromises = []; - oChainObject.aDependentLibraryFiles.forEach(sFile => { - aPromises.push(new Promise(oResolve => { + const aPromises = []; + oChainObject.aDependentLibraryFiles.forEach((sFile) => { + aPromises.push(new Promise((oResolve) => { fs.readFile(sFile, 'utf8', (oError, oData) => { oResolve(oError ? false : oData); }); })); }); - return Promise.all(aPromises).then(aValues => { - let oDependentAPIs = {}; + return Promise.all(aPromises).then((aValues) => { + const oDependentAPIs = {}; - aValues.forEach(sData => { + aValues.forEach((sData) => { let oData; try { @@ -880,7 +967,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oChainObject.oDependentAPIs = oDependentAPIs; return oChainObject; - }) + }); } /** @@ -894,9 +981,9 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (!sFAQDir) { return oChainObject; } - let slibName = oChainObject.fileData.library; + const slibName = oChainObject.fileData.library; oChainObject.fileData.symbols.forEach(function(symbol) { - let sfile = symbol.name.replace(slibName, "").replace(/[.]/g, "/") + ".md"; + const sfile = symbol.name.replace(slibName, "").replace(/[.]/g, "/") + ".md"; if (fs.existsSync(path.join(sFAQDir, sfile))) { symbol.hasFAQ = true; } @@ -913,7 +1000,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // If requested, return data instead of writing to FS (required by UI5 Tooling/UI5 Builder) return JSON.stringify(oChainObject.parsedData); } - let sOutputDir = path.dirname(oChainObject.outputFile); + const sOutputDir = path.dirname(oChainObject.outputFile); // Create dir if it does not exist if (!fs.existsSync(sOutputDir)) { @@ -947,7 +1034,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oChainObject.modules = []; if (oChainObject.libraryFileData) { - let $ = cheerio.load(oChainObject.libraryFileData, { + const $ = cheerio.load(oChainObject.libraryFileData, { ignoreWhitespace: true, xmlMode: true, lowerCaseTags: false @@ -963,8 +1050,8 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (oComponent.children.length === 1) { oChainObject.defaultComponent = $(oComponent).text(); } else { - let sCurrentComponentName = $(oComponent).find("name").text(); - let aCurrentModules = []; + const sCurrentComponentName = $(oComponent).find("name").text(); + const aCurrentModules = []; $(oComponent).find("module").each((a, oC) => { aCurrentModules.push($(oC).text().replace(/\//g, ".")); }); @@ -992,9 +1079,9 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, function flattenComponents(oChainObject) { if (oChainObject.modules && oChainObject.modules.length > 0) { oChainObject.customSymbolComponents = {}; - oChainObject.modules.forEach(oComponent => { - let sCurrentComponent = oComponent.componentName; - oComponent.modules.forEach(sModule => { + oChainObject.modules.forEach((oComponent) => { + const sCurrentComponent = oComponent.componentName; + oComponent.modules.forEach((sModule) => { oChainObject.customSymbolComponents[sModule] = sCurrentComponent; }); }); @@ -1076,7 +1163,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oFileData = JSON.parse(oFileData); if (oFileData.explored && oFileData.explored.entities && oFileData.explored.entities.length > 0) { oChainObject.entitiesWithSamples = []; - oFileData.explored.entities.forEach(oEntity => { + oFileData.explored.entities.forEach((oEntity) => { oChainObject.entitiesWithSamples.push(oEntity.id); }); } @@ -1106,22 +1193,20 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, }); } - /* + /** * ===================================================================================================================== * IMPORTANT NOTE: Formatter code is a copy from APIDetail.controller.js with a very little modification and mocking and * code can be significantly improved * ===================================================================================================================== */ - let formatters = { - + const formatters = { _sTopicId: "", _oTopicData: {}, _baseTypes: [ - // TODO this list URGENTLY needs to be replaced by the Type parser and a much smaller list "sap.ui.core.any", "sap.ui.core.object", "sap.ui.core.function", - "sap.ui.core.number", // TODO discuss with Thomas, type does not exist + "sap.ui.core.number", "sap.ui.core.float", "sap.ui.core.int", "sap.ui.core.boolean", @@ -1129,31 +1214,26 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, "sap.ui.core.void", "null", "any", - "any[]", "Error", - "Error[]", "array", "element", "Element", + "HTMLElement", + "Node", + "Attr", "Date", "DomRef", + "jQuery", "jQuery.promise", + "jQuery.event", "QUnit.Assert", "object", "Object", - "object[]", - "object|object[]", - "[object Object][]", - "Array<[object Object]>", - "Array.<[object Object]>", - "Object", - "Object.", "function", "float", "int", "boolean", "string", - "string[]", "number", "map", "promise", @@ -1163,7 +1243,12 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, "Touch", "TouchList", "undefined", - "this" + "this", + "Blob", + "RegExp", + "void", + "ArrayBuffer", + "[object Object]" ], ANNOTATIONS_LINK: 'http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part3-csdl.html', ANNOTATIONS_NAMESPACE_LINK: 'http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/vocabularies/', @@ -1251,7 +1336,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (aParams && aParams.length > 0) { /* We consider only root level parameters so we get rid of all that are not on the root level */ - aParams = aParams.filter(oElem => { + aParams = aParams.filter((oElem) => { return oElem.depth === undefined; }); aParams.forEach(function (element, index, array) { @@ -1487,7 +1572,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, handleExternalUrl: function (sTarget, sText) { // Check if the external domain is SAP hosted - let bSAPHosted = /^https?:\/\/([\w.]*\.)?(?:sap|hana\.ondemand|sapfioritrial)\.com/.test(sTarget); + const bSAPHosted = /^https?:\/\/([\w.]*\.)?(?:sap|hana\.ondemand|sapfioritrial)\.com/.test(sTarget); return this.formatUrlToLink(sTarget, sText, bSAPHosted); }, @@ -1540,7 +1625,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, return true; } if (oSymbol.name === className) { - let oProperty = findProperty(oSymbol, methodName, target); + const oProperty = findProperty(oSymbol, methodName, target); if (oProperty) { sResult = this.createLink({ name: className, @@ -1548,7 +1633,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, }); return true; } - let oMethod = findMethod(oSymbol, methodName, target); + const oMethod = findMethod(oSymbol, methodName, target); if (oMethod) { sResult = this.createLink({ name: oMethod.static ? target : oMethod.name, @@ -1559,7 +1644,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, return true; } - let oEvent = findEvent(oSymbol, methodName); + const oEvent = findEvent(oSymbol, methodName); if (oEvent) { sResult = this.createLink({ name: oEvent.name, @@ -1570,7 +1655,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, return true; } - let oAnnotation = findAnnotation(oSymbol, methodName); + const oAnnotation = findAnnotation(oSymbol, methodName); if (oAnnotation) { sResult = this.createLink({ name: oAnnotation.name, @@ -1595,7 +1680,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Own methods search if (self.name === className && self.methods) { - let oResult = self.methods.find(({name}) => name === methodName); + const oResult = self.methods.find(({name}) => name === methodName); if (oResult) { return this.createLink({ name: oResult.static ? [self.name, oResult.name].join(".") : oResult.name, @@ -1607,26 +1692,28 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, } // Local library symbols - ownLibrary.symbols.find(oSymbol => { + ownLibrary.symbols.find((oSymbol) => { return searchInSymbol.call(this, oSymbol); }); - if (sResult) return sResult; + if (sResult) {return sResult;} // Dependent library symbols - dependencyLibs && Object.keys(dependencyLibs).find(sLib => { - if (sLib === target) { - sResult = this.createLink({ - name: sLib, - text: text + if (dependencyLibs) { + Object.keys(dependencyLibs).find((sLib) => { + if (sLib === target) { + sResult = this.createLink({ + name: sLib, + text: text + }); + return true; + } + const oLib = dependencyLibs[sLib]; + return oLib && oLib.find((oSymbol) => { + return searchInSymbol.call(this, oSymbol); }); - return true; - } - let oLib = dependencyLibs[sLib]; - return oLib && oLib.find(oSymbol => { - return searchInSymbol.call(this, oSymbol); }); - }); + } return sResult; }, @@ -1641,7 +1728,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, * @param {string} [hrefAppend=""] * @returns {string} link */ - createLink: function ({name, type, className, text=name, hrefAppend=""}) { + createLink: function ({name, type, className, text = name, hrefAppend = ""}) { let sLink; // handling module's @@ -1667,13 +1754,12 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, * @private */ _preProcessLinksInTextBlock: function (sText, bSkipParagraphs) { - let oSelf = this._oTopicData, + const oSelf = this._oTopicData, oOwnLibrary = this._oOwnLibrary, oDependencyLibs = oChainObject.oDependentAPIs, oOptions = { linkFormatter: function (sTarget, sText) { - let aMatch, - aTarget; + let aMatch; // keep the full target in the fallback text sText = sText || sTarget; @@ -1711,7 +1797,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // module:sap/x/Xxx.extend aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)\.extend$/); if (aMatch) { - let [, sModule, sClass] = aMatch; + const [, sModule, sClass] = aMatch; return this.createLink({ name: sTarget.replace(/^module:/, ""), type: "methods", @@ -1727,13 +1813,13 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // #constructor aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)?[\.#]constructor$/i); if (aMatch) { - let [, sModule, sClass] = aMatch, - sName; + const [, sModule, sClass] = aMatch; + let sName; if (sClass) { sName = (sModule ? sModule : "") + sClass; } else { - sName = oSelf.name + sName = oSelf.name; } return this.createLink({ @@ -1773,7 +1859,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // module:sap/ui/comp/smartfield/SmartField#annotation:TextArrangement aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)[.#]annotation:([a-zA-Z0-9$_]+)$/); if (aMatch) { - let [, sModule, sClass, sAnnotation] = aMatch; + const [, sModule, sClass, sAnnotation] = aMatch; return this.createLink({ name: sAnnotation, type: "annotations", @@ -1799,7 +1885,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // module:sap/m/Button#event:press aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)[.#]event:([a-zA-Z0-9$_]+)$/); if (aMatch) { - let [, sModule, sClass, sEvent] = aMatch; + const [, sModule, sClass, sEvent] = aMatch; return this.createLink({ name: sEvent, type: "events", @@ -1812,7 +1898,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // module:sap/m/Button#setText aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+)#([a-zA-Z0-9.$_]+)$/); if (aMatch) { - let [, sModule, sClass, sMethod] = aMatch; + const [, sModule, sClass, sMethod] = aMatch; return this.createLink({ name: sMethod, type: "methods", @@ -1825,8 +1911,8 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // sap.x.Xxx.xxx // module:sap/x/Xxx.xxx if (/^(?:module:)?([a-zA-Z0-9.$_\/]+?)\.([a-zA-Z0-9$_]+)$/.test(sTarget)) { - let [,sClass, sName] = sTarget.match(/^((?:module:)?[a-zA-Z0-9.$_\/]+?)\.([a-zA-Z0-9$_]+)$/), - sResult = this.createLinkFromTargetType({ + const [,sClass, sName] = sTarget.match(/^((?:module:)?[a-zA-Z0-9.$_\/]+?)\.([a-zA-Z0-9$_]+)$/); + const sResult = this.createLinkFromTargetType({ className: sClass, methodName: sName, target: sTarget, @@ -1841,9 +1927,9 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, } // Possible nested functions discovery - currently we do this only for regular symbols - aTarget = sTarget.split("."); + const aTarget = sTarget.split("."); if (aTarget.length >= 3) { - let sResult = this.createLinkFromTargetType({ + const sResult = this.createLinkFromTargetType({ methodName: aTarget.splice(-2).join("."), className: aTarget.join("."), target: sTarget, @@ -1972,7 +2058,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, var bHeaderDocuLinkFound = false, bUXGuidelinesLinkFound = false, aReferences = [], - entity = bCalledOnConstructor? oSymbol.constructor.references : oSymbol.references; + entity = bCalledOnConstructor ? oSymbol.constructor.references : oSymbol.references; const UX_GUIDELINES_BASE_URL = "https://experience.sap.com/fiori-design-web/"; @@ -2016,7 +2102,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, aParts = sReference.match(/^(?:{@link\s)?fiori:(?:https:\/\/experience\.sap\.com\/fiori-design-web\/)?\/?(\S+\b)\/?\s?(.*[^\s}])?}?$/); if (aParts) { - let [, sTarget, sTargetName] = aParts; + const [, sTarget, sTargetName] = aParts; if (bCalledOnConstructor && !bUXGuidelinesLinkFound) { // Extract first found UX Guidelines link as primary @@ -2032,9 +2118,11 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, aReferences.push(sReference); }); - bCalledOnConstructor? oSymbol.constructor.references = aReferences : oSymbol.references = aReferences; + } + if (bCalledOnConstructor) { + oSymbol.constructor.references = aReferences; } else { - bCalledOnConstructor? oSymbol.constructor.references = [] : oSymbol.references = []; + oSymbol.references = aReferences; } }, @@ -2045,13 +2133,13 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, */ formatReferencesInDescription: function(oEntity) { if (oEntity.references && Array.isArray(oEntity.references)) { - oEntity.references = oEntity.references.map(sReference => { + oEntity.references = oEntity.references.map((sReference) => { if (/{@link.*}/.test(sReference)) { return "
  • " + sReference + "
  • "; } else { return "
  • {@link " + sReference + "}
  • "; - }; + } }); if (!oEntity.description) { // If there is no method description - references should be the first line of it @@ -2065,7 +2153,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, }; /* Methods direct copy from API Detail */ - let methods = { + const methods = { /** * Adjusts methods info so that it can be easily displayed in a table @@ -2081,7 +2169,9 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Handle types if (oProperty.type) { - oProperty.types = fnCreateTypesArr(oProperty.type); + oProperty.typeInfo = parseUI5Types(oProperty.type); + // Keep file size in check + delete oProperty.type; } // Phone name - available only for parameters @@ -2111,12 +2201,11 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (oMethod.parameters) { oMethod.parameters.forEach(function (oParameter) { if (oParameter.type) { - oParameter.types = fnCreateTypesArr(oParameter.type); + oParameter.typeInfo = parseUI5Types(oParameter.type); + // Keep file size in check + delete oParameter.type; } - // Keep file size in check - delete oParameter.type; - // Add the parameter before the properties aParameters.push(oParameter); @@ -2133,7 +2222,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Handle return values if (oMethod.returnValue && oMethod.returnValue.type) { // Handle types - oMethod.returnValue.types = fnCreateTypesArr(oMethod.returnValue.type); + oMethod.returnValue.typeInfo = parseUI5Types(oMethod.returnValue.type); } }); @@ -2266,7 +2355,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, }; // Create the chain object - let oChainObject = { + const oChainObject = { inputFile: sInputFile, outputFile: sOutputFile, libraryFile: sLibraryFile, @@ -2274,7 +2363,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, }; // Start the work here - let p = getLibraryPromise(oChainObject) + const p = getLibraryPromise(oChainObject) .then(extractComponentAndDocuindexUrl) .then(flattenComponents) .then(extractSamplesFromDocuIndex) @@ -2286,7 +2375,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, .then(createApiRefApiJson); return p; -}; +} module.exports = transformer; diff --git a/lib/processors/jsdoc/lib/ui5/plugin.js b/lib/processors/jsdoc/lib/ui5/plugin.js index b2e4eb7d9..1117e2c14 100644 --- a/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/lib/processors/jsdoc/lib/ui5/plugin.js @@ -5,9 +5,7 @@ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ -/* global global, env */ -/* eslint-env es6,node */ -/* eslint strict: [2, "global"] */ +/* global env */ 'use strict'; @@ -49,8 +47,10 @@ * newDoclet * * parseComplete - * remove undocumented/ignored/private doclets or duplicate doclets + * merge collected class info, DataType info and enum values into doclets * + * processingComplete + * remove undocumented/ignored/private doclets or duplicate doclets * * Last but not least, it implements an astNodeVisitor to detect UI5 specific "extend" calls and to create * documentation for the properties, aggregations etc. that are created with the "extend" call. @@ -189,6 +189,7 @@ const designtimeInfos = Object.create(null); /* ---- private functions ---- */ function ui5data(doclet) { + // eslint-disable-next-line no-return-assign return doclet.__ui5 || (doclet.__ui5 = { id: ++docletUid }); } @@ -232,7 +233,7 @@ function resolveModuleName(base, name) { let stack = base.split('/'); stack.pop(); name.split('/').forEach((segment, i) => { - if ( segment == '..' ) { + if ( segment === '..' ) { stack.pop(); } else if ( segment === '.' ) { // ignore @@ -283,7 +284,8 @@ function analyzeModuleDefinition(node) { names = [{original: currentModule.factory.params[i].name}]; } - names.forEach(name => { + /*eslint-disable no-loop-func */ + names.forEach((name) => { const module = resolveModuleName(currentModule.module, currentModule.dependencies[i]); debug(` import ${name.renamed || name.original} from '${module}'`); @@ -292,6 +294,7 @@ function analyzeModuleDefinition(node) { ...(name.path ? {path: name.path} : {}) }; }); + /*eslint-enable no-loop-func */ } } if ( currentModule.factory ) { @@ -300,11 +303,11 @@ function analyzeModuleDefinition(node) { } /** - * Searches the given body for variable declarations that can be evaluated statically, + * Searches the body of the given factory for variable declarations that can be evaluated statically, * 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 + * @param {ASTNode} factory AST node of a factory function whose body that shall be searched for shortcuts */ function collectShortcuts(factory) { const body = factory.body; @@ -414,12 +417,13 @@ function guessSingularName(sPluralName) { function getPropertyKey(prop) { if ( prop.type === Syntax.SpreadElement ) { - return; + return undefined; } 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); } + return undefined; } /** @@ -456,14 +460,14 @@ function createPropertyMap(node, defaultKey) { return result; } - if ( node.type != Syntax.ObjectExpression ) { + if ( node.type !== Syntax.ObjectExpression ) { // something went wrong, it's not an object literal warning(`not an object literal: ${node.type}: ${node.value}`); // console.log(node.toSource()); return undefined; } - // invariant: node.type == Syntax.ObjectExpression + // invariant: node.type === Syntax.ObjectExpression result = {}; for (let i = 0; i < node.properties.length; i++) { const prop = node.properties[i]; @@ -478,7 +482,7 @@ function createPropertyMap(node, defaultKey) { /** * Resolves potential wrapper expressions like: ChainExpression, AwaitExpression, etc. - * @param {Node} node + * @param {Node} node the node to unwrap * @returns {Node} the resolved node */ function resolvePotentialWrapperExpression(node) { @@ -495,29 +499,33 @@ function resolvePotentialWrapperExpression(node) { ? node.expression.argument : node.expression; } + // fall through default: return node; } } /** - * Strips the ChainExpression wrapper if such + * Navigates in the given tree of AST nodes (node) along the given path of property names. + * Any reached ChainExpression wrappers are skipped (ignored). * - * @param {Node} rootNode - * @param {String} path - * @returns {Node} + * @param {Node} rootNode Root of a tree of MemberExpressison nodes + * @param {string} path Path to navigate along + * @returns {Node} The target node at the end of the path or undefined */ function stripChainWrappers(rootNode, path) { const strip = (node) => - node && node.type === Syntax.ChainExpression ? node.expression : 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]); + if (path) { + const chunks = path.split("."); + + while (chunks.length) { + const name = chunks.shift(); + curNode = curNode && strip(curNode[name]); + } } return curNode; @@ -535,8 +543,8 @@ function isTemplateLiteralWithoutExpression(node) { /** * Checks whether a node is Literal or TemplateLiteral without an expression * - * @param {Node} node - * @returns {String} + * @param {Node} node AST node to check + * @returns {boolean} Whether the given AST node represents a constant string */ function isStringLiteral(node) { return ( @@ -581,10 +589,10 @@ function isArrowFuncExpression(node) { } /** - * Checks whether the node is of a "returning" type + * Checks whether the node is of a "returning" type. * - * @param {Node} node - * @returns {Boolean} + * @param {Node} node A statement node + * @returns {boolean} Whether the node is a return stmt or a yield expression statement */ function isReturningNode(node) { return (node && node.type === Syntax.ReturnStatement) @@ -695,7 +703,7 @@ function resolveObjectPatternChain (valueNode, keyNode, keyChain) { } } else { - let result = { original: keyNode.name, path: keyChain.join(".") }; + const result = { original: keyNode.name, path: keyChain.join(".") }; if (keyNode.name !== valueNode.name) { // Renaming @@ -712,8 +720,8 @@ function resolveObjectPatternChain (valueNode, keyNode, keyChain) { * Tries to resolve an ENUM, regardless where it is defined and being destructured. * * @param {Node} node - * @param {String} type - * @returns {Object} + * @param {string} type + * @returns {{value:any, raw: any} | undefined} */ function resolvePotentialEnum(node, type) { let value = resolveFullyQuantifiedName(node); @@ -726,13 +734,14 @@ function resolvePotentialEnum(node, type) { raw: value }; } + return undefined; } /** - * Returns the {Node} of the destructured argument of a (arrow) function. + * Returns the `Node` of the destructured argument of a (arrow) function. * * @param {Definition|ParameterDefinition} varDefinition - * @returns {Node} + * @returns {Node | undefined} */ function getFuncArgumentDestructNode(varDefinition) { if ( @@ -745,14 +754,14 @@ function getFuncArgumentDestructNode(varDefinition) { return varDefinition.node.params[varDefinition.index]; } - // return undefined; + return undefined; } /** * Checks whether a variable has been destructured. * * @param {Variable} variable - * @returns + * @returns {boolean} whether `variable` uses destructuring. */ function isVarDestructuring(variable) { const defNode = @@ -761,7 +770,7 @@ function isVarDestructuring(variable) { ( getFuncArgumentDestructNode(variable.defs[0]) // (arrow) function argument || variable.defs[0].node.id ); // variable definition - return defNode && [Syntax.ObjectPattern, Syntax.ArrayPattern].includes( defNode.type ); + return defNode != null && [Syntax.ObjectPattern, Syntax.ArrayPattern].includes( defNode.type ); } /** @@ -926,7 +935,7 @@ function getObjectName(node) { /* * Checks whether the node is a qualified name (a.b.c) and if so, - * returns the leftmost identifier a + * returns the leftmost identifier 'a'. */ function getLeftmostName(node) { while ( isMemberExpression(node) ) { @@ -935,7 +944,7 @@ function getLeftmostName(node) { if ( node.type === Syntax.Identifier ) { return node.name; } - // return undefined; + return undefined; } function getResolvedObjectName(node) { @@ -1076,7 +1085,7 @@ function convertValueWithRaw(node, type, propertyName) { } } else if ( isTemplateLiteralWithoutExpression(node) ) { - let value = node.quasis[0].value || {}; + const value = node.quasis[0].value || {}; return { value: value.cooked, @@ -1145,7 +1154,7 @@ function collectVisibilityInfo(settings, doclet, className, n) { let visibility = (settings.visibility && settings.visibility.value.value) || "public"; if (!validVisibilities.has(visibility)) { - error(`${className}: Invalid visibility '${visibility}' in runtime metadata defined for managed setting '${n}. Valid options are ${Array.from(validVisibilities).join(', ')}.`); + error(`${className}: Invalid visibility '${visibility}' in runtime metadata defined for managed setting '${n}'. Valid options are ${Array.from(validVisibilities).join(', ')}.`); } if (doclet?.access) { @@ -1170,7 +1179,7 @@ function collectVisibilityInfo(settings, doclet, className, n) { access = "restricted"; } - if (visibility == "public" && (access === "restricted" || access === "protected")) { + if (visibility === "public" && (access === "restricted" || access === "protected")) { visibility = access; } } @@ -1220,8 +1229,8 @@ function collectClassInfo(extendCall, classDoclet) { function each(node, defaultKey, callback) { const map = node && createPropertyMap(node.value); if ( map ) { - for (let n in map ) { - if ( map.hasOwnProperty(n) ) { + for (const n in map) { + if ( Object.hasOwn(map, n) ) { const doclet = getLeadingDoclet(map[n]); const settings = createPropertyMap(map[n].value, defaultKey); if ( settings == null ) { @@ -1454,8 +1463,8 @@ function collectDesigntimeInfo(dtNodeArgument) { function each(node, defaultKey, callback) { const map = node && createPropertyMap(node.value); if ( map ) { - for (let n in map ) { - if ( map.hasOwnProperty(n) ) { + for (const n in map) { + if ( Object.hasOwn(map, n) ) { const doclet = getLeadingDoclet(map[n], true); const settings = createPropertyMap(map[n].value, defaultKey); if ( settings == null ) { @@ -1645,7 +1654,7 @@ function collectDataTypeInfo(extendCall, classDoclet) { && stmt.alternate.body[0].argument && stmt.alternate.body[0].argument.type === Syntax.Literal && typeof stmt.alternate.body[0].argument.value === 'boolean' - && stmt.consequent.body[0].argument.value !== typeof stmt.alternate.body[0].argument.value ) { + && stmt.consequent.body[0].argument.value !== stmt.alternate.body[0].argument.value ) { const inverse = stmt.alternate.body[0].argument.value; range = determineValueRange(stmt.test, varname, inverse); } else { @@ -1679,8 +1688,8 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, if ( !obj ) { return true; } - for (let n in obj) { - if ( obj.hasOwnProperty(n) ) { + for (const n in obj) { + if ( Object.hasOwn(obj, n) ) { return false; } } @@ -1868,8 +1877,8 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, if ( !isEmpty(oClassInfo.properties) ) { lines.push("
  • Properties"); lines.push("
      "); - for (let n in oClassInfo.properties) { - lines.push("
    • {@link " + rname("get", n) + " " + n + "} : " + oClassInfo.properties[n].type + (oClassInfo.properties[n].defaultValue !== null && oClassInfo.properties[n].defaultValue.value !== null ? " (default: " + oClassInfo.properties[n].defaultValue.raw + ")" : "") + (oClassInfo.defaultProperty == n ? " (default)" : "") + "
    • "); + for (const n in oClassInfo.properties) { + lines.push("
    • {@link " + rname("get", n) + " " + n + "} : " + oClassInfo.properties[n].type + (oClassInfo.properties[n].defaultValue !== null && oClassInfo.properties[n].defaultValue.value !== null ? " (default: " + oClassInfo.properties[n].defaultValue.raw + ")" : "") + (oClassInfo.defaultProperty === n ? " (default)" : "") + "
    • "); } lines.push("
    "); lines.push("
  • "); @@ -1877,9 +1886,9 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, if ( !isEmpty(oClassInfo.aggregations) ) { lines.push("
  • Aggregations"); lines.push("
      "); - for (let n in oClassInfo.aggregations) { + for (const n in oClassInfo.aggregations) { if ( oClassInfo.aggregations[n].visibility !== "hidden" ) { - lines.push("
    • {@link " + rname("get", n) + " " + n + "} : " + makeTypeString(oClassInfo.aggregations[n]) + (oClassInfo.defaultAggregation == n ? " (default)" : "") + "
    • "); + lines.push("
    • {@link " + rname("get", n) + " " + n + "} : " + makeTypeString(oClassInfo.aggregations[n]) + (oClassInfo.defaultAggregation === n ? " (default)" : "") + "
    • "); } } lines.push("
    "); @@ -1888,7 +1897,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, if ( !isEmpty(oClassInfo.associations) ) { lines.push("
  • Associations"); lines.push("
      "); - for (let n in oClassInfo.associations) { + for (const n in oClassInfo.associations) { lines.push("
    • {@link " + rname("get", n) + " " + n + "} : (sap.ui.core.ID | " + oClassInfo.associations[n].type + ")" + (oClassInfo.associations[n].cardinality === "0..n" ? "[]" : "") + "
    • "); } lines.push("
    "); @@ -1897,7 +1906,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, if ( !isEmpty(oClassInfo.events) ) { lines.push("
  • Events"); lines.push("
      "); - for (let n in oClassInfo.events) { + for (const n in oClassInfo.events) { lines.push("
    • {@link " + "#event:" + n + " " + n + "} : fnListenerFunction or [fnListenerFunction, oListenerObject] or [oData, fnListenerFunction, oListenerObject]
    • "); } lines.push("
    "); @@ -1972,7 +1981,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, ]); } - for (let n in oClassInfo.properties ) { + for (const n in oClassInfo.properties ) { const info = oClassInfo.properties[n]; if ( info.visibility === 'hidden' ) { continue; @@ -2044,7 +2053,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, } } - for (let n in oClassInfo.aggregations ) { + for (const n in oClassInfo.aggregations ) { const info = oClassInfo.aggregations[n]; if ( info.visibility === 'hidden' ) { continue; @@ -2067,7 +2076,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, "@name " + name("get", n), "@function" ]); - if ( info.cardinality == "0..n" ) { + if ( info.cardinality === "0..n" ) { const n1 = info.singularName; newJSDoc([ "Inserts a " + n1 + " into the aggregation " + link + ".", @@ -2188,7 +2197,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, } } - for (let n in oClassInfo.associations ) { + for (const n in oClassInfo.associations ) { const info = oClassInfo.associations[n]; if ( info.visibility === 'hidden' ) { continue; @@ -2262,7 +2271,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, } } - for (let n in oClassInfo.events ) { + for (const n in oClassInfo.events ) { const info = oClassInfo.events[n]; const visibilityTags = createVisibilityTagsForSetting(info, classAccess); @@ -2285,7 +2294,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, "@param {sap.ui.base.EventProvider} oControlEvent.getSource", "@param {object} oControlEvent.getParameters" ]; - for (let pName in info.parameters ) { + for (const pName in info.parameters ) { lines.push( "@param {" + (info.parameters[pName].type || "") + "} oControlEvent.getParameters." + pName + " " + (info.parameters[pName].doc || "") ); @@ -2351,7 +2360,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, "@param {object} [mParameters] Parameters to pass along with the event" ); if ( !isEmpty(info.parameters) ) { - for (let pName in info.parameters) { + for (const pName in info.parameters) { lines.push( "@param {" + (info.parameters[pName].type || "any") + "} [mParameters." + pName + "] " + (info.parameters[pName].doc || "") ); @@ -2426,6 +2435,7 @@ if ( Syntax.File === 'File' ) { return leadingComments[leadingComments.length - 1]; } } + return undefined; }; } else { @@ -2573,7 +2583,7 @@ function preprocessComment(e) { // HACK: override cli.exit() to avoid that JSDoc3 exits the VM if ( pluginConfig.noExit ) { info("disabling exit() call"); - require( path.join(global.env.dirname, 'cli') ).exit = function(retval) { + require( path.join(env.dirname, 'cli') ).exit = function(retval) { info(`cli.exit(): do nothing (ret val=${retval})`); }; } @@ -2915,53 +2925,20 @@ exports.handlers = { currentSource = e.source; }, + /** + * Event `parseComplete` is fired by JSDoc after all files have been parsed, + * but before inheritance, mixins or borrows are processed. + * + * We use this event to merge our additional data into the doclets collected by JSDoc. + * The merge must happen before doclets are cloned during the prcessing of augments or borrows. + */ parseComplete : function(e) { - const doclets = e.doclets; - const rAnonymous = /^(~|$)/; - - // remove undocumented symbols, ignored symbols, anonymous functions and their members, scope members - let l = doclets.length, i, j; - for (i = 0, j = 0; i < l; i++) { - - const doclet = doclets[i]; - if ( !doclet.undocumented && - !doclet.ignore && - !(doclet.memberof && rAnonymous.test(doclet.memberof)) && - doclet.longname.indexOf("~") < 0 ) { - doclets[j++] = doclet; - } - } - if ( j < l ) { - doclets.splice(j, l - j); - info(`removed ${l - j} undocumented, ignored or anonymous symbols`); - l = j; - } - - // sort doclets by name, synthetic, lineno, uid - // 'ignore' is a combination of criteria, see function above - debug("sorting doclets by name"); - doclets.sort((a, b) => { - if ( a.longname === b.longname ) { - if ( a.synthetic === b.synthetic ) { - if ( a.meta && b.meta && a.meta.filename == b.meta.filename ) { - if ( a.meta.lineno !== b.meta.lineno ) { - return a.meta.lineno < b.meta.lineno ? -1 : 1; - } - } - return a.__ui5.id - b.__ui5.id; - } - return a.synthetic && !b.synthetic ? -1 : 1; - } - return a.longname < b.longname ? -1 : 1; - }); - debug("sorting doclets by name done."); - - for (i = 0, j = 0; i < l; i++) { - + const l = doclets.length; + for (let i = 0; i < l; i++) { const doclet = doclets[i]; - // add metadata to symbol + // add metadata to class symbols if ( classInfos[doclet.longname] ) { // debug("class data", doclet.longname, "'" + classInfos[doclet.longname].export + "'"); if ( doclet.__ui5.export === undefined ) { @@ -3005,6 +2982,7 @@ exports.handlers = { } } + // add DataType info to typedef symbols if ( typeInfos[doclet.longname] ) { doclet.__ui5.stereotype = 'datatype'; doclet.__ui5.metadata = { @@ -3014,6 +2992,7 @@ exports.handlers = { }; } + // add enum values to enum keys (for enum symbols) if ( (doclet.kind === 'member' || doclet.kind === 'constant') && doclet.isEnum && Array.isArray(doclet.properties) ) { // determine unique enum identifier from key set let enumID = doclet.properties.map(function(prop) { @@ -3036,31 +3015,96 @@ exports.handlers = { } } } + } + }, + + /** + * Event `processingComplete` is fired by JSDoc after all files have been parsed, + * and after inheritance, mixins and borrows have been processed. + * + * The `e.doclets` contains the symbols that will be given to templates for publishing. + * + * We use this event to remove symbols that are not of interest: + * - undocumented When JSDoc finds a class, function, object or member without a + * JSDoc comment, it creates a doclet with a truthy `undocumented` + * property + * - ignore A symbol that has been marked with `@ignore` in the source code + * - anonymous JSDoc could not infer a name for the symbol, neither from source + * code nor from JSDoc comments + * - local Local entities (e.g. local vars) can't be addressed from the outside + * and therefore are generally not considered as API in UI5 + * - duplicates This plugin generates doclets for the accessor methods of + * managed properties, aggregations, events, associations. + * Developers might have created JSDoc comments for the same methods, + * either because they have overridden them in code or because they + * wanted to detail the method contract. If such duplicate doclets + * are detected, the developer created doclets are preferred. If + * multiple developer created doclets for the same entity exist in the + * same file, the last one wins. If multiple doclets exists across + * files, the one created last wins (but usually, this indicates a + * copy & paste error) + * + * The cleanup is done in `processingComplete` as the processing of `@augments` or + * `@borrows` tags might have created new non-interesting symbols. + */ + processingComplete(e) { + const doclets = e.doclets; + + // sort doclets by name, synthetic, lineno, uid for easier detection of duplicates + debug("sorting doclets by name"); + doclets.sort((a, b) => { + if ( a.longname === b.longname ) { + if ( a.synthetic === b.synthetic ) { + if ( a.meta && b.meta && a.meta.filename === b.meta.filename ) { + if ( a.meta.lineno !== b.meta.lineno ) { + return a.meta.lineno < b.meta.lineno ? -1 : 1; + } + } + return a.__ui5.id - b.__ui5.id; + } + return a.synthetic && !b.synthetic ? -1 : 1; + } + return a.longname < b.longname ? -1 : 1; + }); + debug("sorting doclets by name done."); + + // cleanup doclets + const rAnonymous = /^(~|$)/; + const l = doclets.length; + let j = 0; + for (let i = 0; i < l; i++) { + const doclet = doclets[i]; + + // skip undocumented, ignored, anonymous entities as well as local entities + if (doclet.undocumented + || doclet.ignore + || (doclet.memberof && rAnonymous.test(doclet.memberof)) + || doclet.longname.includes("~") ) { + continue; + } // check for duplicates: last one wins if ( j > 0 && doclets[j - 1].longname === doclet.longname ) { if ( !doclets[j - 1].synthetic && !doclet.__ui5.updatedDoclet ) { - // replacing synthetic comments or updating comments are trivial case. Just log non-trivial duplicates + // replacing synthetic comments or updating comments are trivial cases. Just log non-trivial duplicates debug(`ignoring duplicate doclet for ${doclet.longname}: ${location(doclet)} overrides ${location(doclets[j - 1])}`); } doclets[j - 1] = doclet; - } else { - doclets[j++] = doclet; + continue; } + + doclets[j++] = doclet; } if ( j < l ) { doclets.splice(j, l - j); - info(`removed ${l - j} duplicate symbols - ${doclets.length} remaining`); + info(`processingComplete: removed ${l - j} undocumented, ignored, anonymous, local or duplicate symbols - ${doclets.length} remaining`); } if ( pluginConfig.saveSymbols ) { - fs.mkPath(env.opts.destination); - fs.writeFileSync(path.join(env.opts.destination, "symbols-parseComplete.json"), JSON.stringify(e.doclets, null, "\t"), 'utf8'); - + fs.writeFileSync(path.join(env.opts.destination, "symbols-processingComplete.json"), JSON.stringify(e.doclets, null, "\t"), 'utf8'); } - } }; @@ -3122,9 +3166,11 @@ exports.astNodeVisitor = { nodeToAnalyze = node.right; } - nodeToAnalyze && analyzeModuleDefinition( - resolvePotentialWrapperExpression(nodeToAnalyze) - ); + if (nodeToAnalyze) { + analyzeModuleDefinition( + resolvePotentialWrapperExpression(nodeToAnalyze) + ); + } } const isArrowExpression = isArrowFuncExpression(node) && node.body.type === Syntax.ObjectExpression; @@ -3197,7 +3243,7 @@ exports.astNodeVisitor = { let programBodyStart; if ( node.program.body.length >= 1 ) { - programBodyStart = node.program.body[0].start + programBodyStart = node.program.body[0].start; } else { // File has no code at all programBodyStart = Infinity; @@ -3236,6 +3282,7 @@ exports.astNodeVisitor = { function getTypeStrings(parsedType, isOutermostType) { let applications; let typeString; + let paramTypes; let types = []; switch (parsedType.type) { case TYPES.AllLiteral: @@ -3244,7 +3291,7 @@ exports.astNodeVisitor = { case TYPES.FunctionType: typeString = 'function'; // #### BEGIN: MODIFIED BY SAP - const paramTypes = []; + paramTypes = []; if (parsedType.new) { paramTypes.push(toTypeString(parsedType.new)); } @@ -3291,7 +3338,7 @@ exports.astNodeVisitor = { const realName = keyString.substring(0, pos); const realValue = keyString.substring(pos + 1, keyString.length); let message = `Cannot parse the "${keyString}" part of "${parsedType.typeExpression}" in RecordType (log output above may give a hint in which file).\n`; - message += `Did you mean to specify a property "${realName}" of type "${realValue}"? Then insert a space after the colon and write "${realName}: ${realValue}".` + message += `Did you mean to specify a property "${realName}" of type "${realValue}"? Then insert a space after the colon and write "${realName}: ${realValue}".`; error(message); return "x: any"; // unparseable property set to "any" - but the JSDoc run will fail now, anyway } else { @@ -3310,17 +3357,16 @@ exports.astNodeVisitor = { case TYPES.TypeApplication: // if this is the outermost type, we strip the modifiers; otherwise, we keep them if (isOutermostType) { - applications = parsedType.applications.map(application => + applications = parsedType.applications.map((application) => catharsis.stringify(application)).join(', '); typeString = `${getTypeStrings(parsedType.expression)[0]}.<${applications}>`; types.push(typeString); - } - else { + } else { types.push( catharsis.stringify(parsedType) ); } break; case TYPES.TypeUnion: - parsedType.elements.forEach(element => { + parsedType.elements.forEach((element) => { types = types.concat( getTypeStrings(element) ); }); break; @@ -3350,5 +3396,5 @@ exports.astNodeVisitor = { // console.info("new parse result", tagInfo.type); } return tagInfo; - } + }; }()); diff --git a/lib/processors/jsdoc/lib/ui5/template/publish.js b/lib/processors/jsdoc/lib/ui5/template/publish.js index 6fd7f52ef..b7787faea 100644 --- a/lib/processors/jsdoc/lib/ui5/template/publish.js +++ b/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -5,9 +5,7 @@ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ -/* global env: true */ -/* eslint-env es6,node */ -/* eslint strict: [2, "global"] */ +/* global env */ "use strict"; @@ -27,6 +25,7 @@ const warning = logger.warn.bind(logger); const error = logger.error.bind(logger); const {extractVersion, extractSince} = require("./utils/versionUtil"); +const {ASTBuilder, TypeParser} = require("./utils/typeParser"); /* errors that might fail the build in future */ function future(msg) { @@ -42,39 +41,7 @@ function future(msg) { /* globals, constants */ const MY_TEMPLATE_NAME = "ui5", MY_ALT_TEMPLATE_NAME = "sapui5-jsdoc3", - ANONYMOUS_LONGNAME = doclet.ANONYMOUS_LONGNAME, - A_SECURITY_TAGS = [ - { - name : "SecSource", - caption : "Taint Source", - description : "APIs that might introduce tainted data into an application, e.g. due to user input or network access", - params : ["out","flags"] - }, - { - name : "SecEntryPoint", - caption : "Taint Entry Point", - description: "APIs that are called implicitly by a framework or server and trigger execution of application logic", - params : ["in","flags"] - }, - { - name : "SecSink", - caption : "Taint Sink", - description : "APIs that pose a security risk when they receive tainted data", - params : ["in","flags"] - }, - { - name : "SecPassthrough", - caption : "Taint Passthrough", - description : "APIs that might propagate tainted data when they receive it as input", - params : ["in","out","flags"] - }, - { - name : "SecValidate", - caption : "Validation", - description : "APIs that (partially) cleanse tainted data so that it no longer poses a security risk in the further data flow of an application", - params : ["in","out","flags"] - } - ]; + ANONYMOUS_LONGNAME = doclet.ANONYMOUS_LONGNAME; const templatesConf = (env.conf.templates || {}), templateConf = templatesConf[MY_TEMPLATE_NAME] || templatesConf[MY_ALT_TEMPLATE_NAME] || {}; @@ -83,7 +50,7 @@ let conf = {}; let __symbols; let __longnames; -let __missingLongnames = {}; +const __missingLongnames = {}; function merge(target, source) { if ( source != null ) { @@ -119,7 +86,7 @@ function lookup(key) { function createSymbol(longname, lines = []) { const comment = [ "@name " + longname, - ... lines + ...lines ]; const symbol = new doclet.Doclet("/**\n * " + comment.join("\n * ") + "\n */", {}); @@ -215,7 +182,7 @@ function writeSymbols(symbols, filename, caption) { v.base = value.base.longname; } if ( value.implementations ) { - v.base = value.implementations.map(($)=> $.longname); + v.base = value.implementations.map(($) => $.longname); } if ( value.parent ) { v.parent = value.parent.longname; @@ -454,7 +421,7 @@ function createInheritanceTree(allSymbols) { } else { warning(`create missing class ${sClass} (extended by ${sExtendingClass})`); } - let oBaseClass = getOrCreateClass(sBaseClass, sClass); + const oBaseClass = getOrCreateClass(sBaseClass, sClass); oClass = createSymbol(sClass, [ "@extends " + sBaseClass, "@" + sKind, @@ -485,7 +452,7 @@ function createInheritanceTree(allSymbols) { aRootTypes.push(oClass); } - let oBaseClass = getOrCreateClass(sBaseClass, oClass.longname); + const oBaseClass = getOrCreateClass(sBaseClass, oClass.longname); oClass.__ui5.base = oBaseClass; oBaseClass.__ui5.derived = oBaseClass.__ui5.derived || []; oBaseClass.__ui5.derived.push(oClass); @@ -667,60 +634,8 @@ function sortByAlias(a, b) { return 0; } -/* Make a symbol sorter by some attribute. */ -function makeSortby(/* fields ...*/) { - const aFields = Array.prototype.slice.apply(arguments), - aNorms = [], - aFuncs = []; - for (let i = 0; i < arguments.length; i++) { - aNorms[i] = 1; - if ( typeof aFields[i] === 'function' ) { - aFuncs[i] = aFields[i]; - continue; - } - aFuncs[i] = function($,n) { return $[n]; }; - if ( aFields[i].indexOf("!") === 0 ) { - aNorms[i] = -1; - aFields[i] = aFields[i].slice(1); - } - if ( aFields[i] === 'deprecated' ) { - aFuncs[i] = function($,n) { return !!$[n]; }; - } else if ( aFields[i] === 'static' ) { - aFields[i] = 'scope'; - aFuncs[i] = function($,n) { return $[n] === 'static'; }; - } else if ( aFields[i].indexOf("#") === 0 ) { - aFields[i] = aFields[i].slice(1); - aFuncs[i] = function($,n) { return $.comment.getTag(n).length > 0; }; - } - } - return function(a, b) { - // debug(`compare ${a.longname} : ${b.longname}`); - let r = 0; - for (let i = 0; r === 0 && i < aFields.length; i++) { - let va = aFuncs[i](a,aFields[i]); - let vb = aFuncs[i](b,aFields[i]); - if ( va && !vb ) { - r = -aNorms[i]; - } else if ( !va && vb ) { - r = aNorms[i]; - } else if ( va && vb ) { - va = String(va).toLowerCase(); - vb = String(vb).toLowerCase(); - if (va < vb) { - r = -aNorms[i]; - } - if (va > vb) { - r = aNorms[i]; - } - } - // debug(` ${aFields[i]}: ${va} ? ${vb} = ${r}`); - } - return r; - }; -} - function getMembersOfKind(data, kind) { - let oResult = []; + const oResult = []; //debug(`calculating kind ${kind} from ${data.longname}`); //console.log(data); let fnFilter; @@ -759,94 +674,6 @@ function getMembersOfKind(data, kind) { // ---- type parsing --------------------------------------------------------------------------------------------- -class ASTBuilder { - literal(str) { - return { - type: "literal", - value: str - }; - } - simpleType(type) { - return { - type: "simpleType", - name: type - }; - } - array(componentType) { - return { - type: "array", - component: componentType - }; - } - object(keyType, valueType) { - return { - type: "object", - key: keyType, - value: valueType - }; - } - set(elementType) { - return { - type: "set", - element: elementType - }; - } - promise(fulfillmentType) { - return { - type: "promise", - fulfill: fulfillmentType - }; - } - "function"(paramTypes, returnType, thisType, constructorType) { - return { - "type": "function", - "params": paramTypes, - "return": returnType, - "this": thisType, - "constructor": constructorType - }; - } - structure(structure) { - return { - type: "structure", - fields: structure - }; - } - union(types) { - return { - type: "union", - types: types - }; - } - synthetic(type) { - type.synthetic = true; - return type; - } - nullable(type) { - type.nullable = true; - return type; - } - mandatory(type) { - type.mandatory = true; - return type; - } - optional(type) { - type.optional = true; - return type; - } - repeatable(type) { - type.repeatable = true; - return type; - } - typeApplication(type, templateTypes) { - return { - type: "typeApplication", - baseType: type, - templateTypes: templateTypes - }; - } -} - class TypeStringBuilder { constructor() { this.lt = "<"; @@ -906,7 +733,7 @@ class TypeStringBuilder { "function"(paramTypes, returnType) { return { simpleComponent: false, - str: "function(" + paramTypes.map(type => type.str).join(',') + ")" + ( returnType ? " : " + this.safe(returnType) : "") + str: "function(" + paramTypes.map((type) => type.str).join(',') + ")" + ( returnType ? " : " + this.safe(returnType) : "") }; } structure(structure) { @@ -934,7 +761,7 @@ class TypeStringBuilder { union(types) { return { needsParenthesis: true, - str: types.map(type => this.safe(type)).join('|') + str: types.map((type) => this.safe(type)).join('|') }; } synthetic(type) { @@ -960,248 +787,11 @@ class TypeStringBuilder { typeApplication(type, templateTypes) { return { simpleComponent: false, - str: this.safe(type) + this.lt + templateTypes.map(type => this.safe(type)).join(',') + this.gt + str: this.safe(type) + this.lt + templateTypes.map((type) => this.safe(type)).join(',') + this.gt }; } } -function TypeParser(defaultBuilder = new ASTBuilder()) { - const rLexer = /\s*(Array\.?<|Object\.?<|Set\.?<|Promise\.?<|function\(|\{|:|\(|\||\}|\.?<|>|\)|,|\[\]|\*|\?|!|=|\.\.\.)|\s*(false|true|(?:\+|-)?(?:\d+(?:\.\d+)?|NaN|Infinity)|'[^']*'|"[^"]*"|null|undefined)|\s*((?:module:)?\w+(?:[/.#~][$\w_]+)*)|./g; - - let input; - let builder; - let token; - let tokenStr; - - function next(expected) { - if ( expected !== undefined && token !== expected ) { - throw new SyntaxError( - `TypeParser: expected '${expected}', but found '${tokenStr}' ` + - `(pos: ${rLexer.lastIndex}, input='${input}')` - ); - } - const match = rLexer.exec(input); - if ( match ) { - tokenStr = match[1] || match[2] || match[3]; - token = match[1] || (match[2] && "literal") || (match[3] && "symbol"); - if ( !token ) { - throw new SyntaxError(`TypeParser: unexpected '${match[0]}' (pos: ${match.index}, input='${input}')`); - } - } else { - tokenStr = token = null; - } - } - - function parseType() { - let nullable = false; - let mandatory = false; - if ( token === "?" ) { - next(); - nullable = true; - } else if ( token === "!" ) { - next(); - mandatory = true; - } - - let type; - - if ( token === "literal" ) { - type = builder.literal(tokenStr); - next(); - } else if ( token === "Array.<" || token === "Array<" ) { - next(); - const componentType = parseTypes(); - next(">"); - type = builder.array(componentType); - } else if ( token === "Object.<" || token === "Object<" ) { - next(); - let keyType; - let valueType = parseTypes(); - if ( token === "," ) { - next(); - keyType = valueType; - valueType = parseTypes(); - } else { - keyType = builder.synthetic(builder.simpleType("string")); - } - next(">"); - type = builder.object(keyType, valueType); - } else if ( token === "Set.<" || token === "Set<" ) { - next(); - const elementType = parseTypes(); - next(">"); - type = builder.set(elementType); - } else if ( token === "Promise.<" || token === "Promise<" ) { - next(); - const resultType = parseTypes(); - next(">"); - type = builder.promise(resultType); - } else if ( token === "function(" ) { - next(); - let thisType; - let constructorType; - const paramTypes = []; - let returnType; - if ( tokenStr === "this" ) { - next(); - next(":"); - thisType = parseType(); - if ( token !== ")" ) { - next(","); - } - } else if ( tokenStr === "new" ) { - next(); - next(":"); - constructorType = parseType(); - if ( token !== ")" ) { - next(","); - } - } - while ( token !== ")" ) { - const repeatable = token === "..."; - if ( repeatable ) { - next(); - } - let paramType = parseTypes(); - if ( repeatable ) { - paramType = builder.repeatable(paramType); - } - const optional = token === "="; - if ( optional ) { - paramType = builder.optional(paramType); - next(); - } - paramTypes.push(paramType); - - // exit if there are no more parameters - if ( token !== "," ) { - break; - } - - if ( repeatable ) { - throw new SyntaxError( - `TypeParser: only the last parameter of a function can be repeatable ` + - `(pos: ${rLexer.lastIndex}, input='${input}')` - ); - } - - // consume the comma - next(); - } - next(")"); - if ( token === ":" ) { - next(":"); - returnType = parseType(); - } - type = builder.function(paramTypes, returnType, thisType, constructorType); - } else if ( token === "{" ) { - const structure = Object.create(null); - next(); - do { - const propName = tokenStr; - if ( !/^\w+$/.test(propName) ) { - throw new SyntaxError( - `TypeParser: structure field must have a simple name ` + - `(pos: ${rLexer.lastIndex}, input='${input}', field:'${propName}')` - ); - } - next("symbol"); - let propType; - const optional = token === "="; - if ( optional ) { - next(); - } - if ( token === ":" ) { - next(); - propType = parseTypes(); - } else { - propType = builder.synthetic(builder.simpleType("any")); - } - if ( optional ) { - propType = builder.optional(propType); - } - structure[propName] = propType; - if ( token === "}" ) { - break; - } - next(","); - } while (token); - next("}"); - type = builder.structure(structure); - } else if ( token === "(" ) { - next(); - type = parseTypes(); - next(")"); - } else if ( token === "*" ) { - next(); - type = builder.simpleType("*"); - } else { - type = builder.simpleType(tokenStr); - next("symbol"); - // check for suffix operators: either 'type application' (generics) or 'array', but not both of them - if ( token === "<" || token === ".<" ) { - next(); - const templateTypes = []; - do { - const templateType = parseTypes(); - templateTypes.push(templateType); - if ( token === ">" ) { - break; - } - next(","); - } while (token); - next(">"); - type = builder.typeApplication(type, templateTypes); - } else { - while ( token === "[]" ) { - next(); - type = builder.array(type); - } - } - } - if ( builder.normalizeType ) { - type = builder.normalizeType(type); - } - if ( nullable ) { - type = builder.nullable(type); - } - if ( mandatory ) { - type = builder.mandatory(type); - } - return type; - } - - function parseTypes() { - const types = []; - do { - types.push(parseType()); - if ( token !== "|" ) { - break; - } - next(); - } while (token); - return types.length === 1 ? types[0] : builder.union(types); - } - - this.parse = function(typeStr, tempBuilder = defaultBuilder) { - /* - try { - const r = catharsis.parse(typeStr, { jsdoc: true}); - console.log(JSON.stringify(typeStr, null, "\t"), r); - } catch (err) { - console.log(typeStr, err); - } - */ - builder = tempBuilder; - input = String(typeStr); - rLexer.lastIndex = 0; - next(); - const type = parseTypes(); - next(null); - return type; - }; -} - const typeParser = new TypeParser(); const _TEXT_BUILDER = new TypeStringBuilder(); @@ -1235,6 +825,7 @@ function _processTypeString(type, builder) { return type; } } + return undefined; } function listTypes(type) { @@ -1341,13 +932,13 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { const obj = []; let curr = obj; let attribForKind = 'kind'; - let stack = []; + const stack = []; function isEmpty(obj) { if ( !obj ) { return true; } - for (let n in obj) { + for (const n in obj) { if ( obj.hasOwnProperty(n) ) { return false; } @@ -1448,7 +1039,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { } if (info.since === null) { future(`**** Failed to parse version in string '${value}'. ` + - `Version might be missing or has an unexpected format.`) + `Version might be missing or has an unexpected format.`); } if ( info.value ) { curr["text"] = normalizeWS(info.value); @@ -1513,7 +1104,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { if ( $.access === 'restricted' ) { return $.__ui5.stakeholders; } - // return undefined + return undefined; } function exceptions(symbol) { @@ -1604,7 +1195,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { if ( metadata.specialSettings && Object.keys(metadata.specialSettings).length > 0 ) { collection("specialSettings"); - for ( let n in metadata.specialSettings ) { + for ( const n in metadata.specialSettings ) { const special = metadata.specialSettings[n]; tag("specialSetting"); attrib("name", special.name); @@ -1625,7 +1216,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { if ( metadata.properties && Object.keys(metadata.properties).length > 0 ) { collection("properties"); - for ( let n in metadata.properties ) { + for ( const n in metadata.properties ) { const prop = metadata.properties[n]; let defaultValue = prop.defaultValue != null ? prop.defaultValue.value : null; // JSON can't transport a value of undefined, so represent it as string @@ -1670,7 +1261,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { if ( metadata.aggregations && Object.keys(metadata.aggregations).length > 0 ) { collection("aggregations"); - for ( let n in metadata.aggregations ) { + for ( const n in metadata.aggregations ) { const aggr = metadata.aggregations[n]; tag("aggregation"); attrib("name", aggr.name); @@ -1707,7 +1298,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { if ( metadata.associations && Object.keys(metadata.associations).length > 0 ) { collection("associations"); - for ( let n in metadata.associations ) { + for ( const n in metadata.associations ) { const assoc = metadata.associations[n]; tag("association"); attrib("name", assoc.name); @@ -1731,7 +1322,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { if ( metadata.events && Object.keys(metadata.events).length > 0 ) { collection("events"); - for ( let n in metadata.events ) { + for ( const n in metadata.events ) { const event = metadata.events[n]; tag("event"); attrib("name", event.name); @@ -1751,7 +1342,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { tagWithSince("deprecated", event.deprecation); if ( event.parameters && Object.keys(event.parameters).length > 0 ) { tag("parameters"); - for ( let pn in event.parameters ) { + for ( const pn in event.parameters ) { if ( event.parameters.hasOwnProperty(pn) ) { const param = event.parameters[pn]; tag(pn); @@ -1774,7 +1365,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { if ( metadata.annotations && Object.keys(metadata.annotations).length > 0 ) { collection("annotations"); - for ( let n in metadata.annotations ) { + for ( const n in metadata.annotations ) { const anno = metadata.annotations[n]; tag("annotation"); attrib("name", anno.name); @@ -2308,7 +1899,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { if ( symbol.__ui5.stereotype !== 'xmlmacro' && ownMethods.length > 0 ) { collection("methods"); ownMethods.forEach(function(member) { - writeMethod(member, undefined, symbol.kind === 'interface' || symbol.kind === 'class'); + writeMethod(member, undefined, symbol.kind === 'interface' || symbol.kind === 'class'); if ( member.__ui5.members ) { // HACK: export nested static functions as siblings of the current function // A correct representation has to be discussed with the SDK / WebIDE @@ -2377,7 +1968,7 @@ function postProcessAPIJSON(api) { function guessExports(moduleName, symbols) { // a non-stringifiable special value for unresolved exports - let UNRESOLVED = function() {}; + const UNRESOLVED = function() {}; symbols = symbols.sort((a,b) => { if ( a.name === b.name ) { @@ -2457,7 +2048,7 @@ function postProcessAPIJSON(api) { } - for ( let n in modules ) { + for ( const n in modules ) { guessExports(n, modules[n]); } @@ -2478,7 +2069,7 @@ function postProcessAPIJSON(api) { } symbol = findSymbol(symbol.extends); } - // return undefined + return undefined; } // See sap/ui/base/ManagedObjectMetadata @@ -2525,6 +2116,37 @@ function postProcessAPIJSON(api) { `doesn't match containing library '${api.library}'. Library must be explicitly defined in class metadata!`); } } + if (Array.isArray(symbol["ui5-metadata"].properties)) { + for (const prop of symbol["ui5-metadata"].properties) { + let moduleType; + // cut off array brackets for symbol lookup + const arrType = isArrayType(prop.type); + let lookupType = prop.type; + if (arrType) { + lookupType = lookupType.replace("[]", ""); + } + if (prop.dataType === undefined + && findSymbol(lookupType) == null + && !lookupType?.startsWith("module:") + && findSymbol(moduleType = `module:${lookupType.replace(/\./g, "/")}`) != null) { + // note: the dataType must be the original, including array brackets if present + prop.dataType = prop.type; + // the moduleType also needs to include the array brackets if present in the original dataType + prop.type = moduleType + (arrType ? "[]" : ""); + for (const methodName of prop.methods) { + const method = symbol.methods?.find((m) => m.name === methodName); + if (methodName.startsWith("get") + && method?.returnValue?.type === prop.dataType) { + method.returnValue.type = prop.type; + } else if (methodName.startsWith("set") + && method?.parameters?.[0]?.type === prop.dataType) { + method.parameters[0].type = prop.type; + } + } + info(`${symbol.name}: adapted type of ${prop.name} and its accessors from '${prop.dataType}' to '${prop.type}'`); + } + } + } }); } @@ -2568,16 +2190,22 @@ const builtinTypes = { Attr:true, Blob:true, DataTransfer:true, + DragEvent:true, Document:true, DOMException:true, + DOMRect:true, Element:true, Event:true, File:true, FileList:true, + FormData:true, Headers:true, HTMLDocument:true, HTMLElement:true, Node:true, + PerformanceResourceTiming:true, + Request:true, + Response:true, Storage:true, Touch:true, TouchList:true, @@ -2628,7 +2256,9 @@ function validateAPIJSON(api) { // create map of defined symbols (built-in types, dependency libraries, current library) const defined = Object.assign(Object.create(null), builtinTypes, externalSymbols); if ( api.symbols ) { - api.symbols.forEach((symbol) => defined[symbol.name] = symbol); + api.symbols.forEach((symbol) => { + defined[symbol.name] = symbol; + }); } const naming = Object.create(null); @@ -2783,12 +2413,10 @@ function validateAPIJSON(api) { 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 + } 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 ) { @@ -2830,7 +2458,7 @@ function validateAPIJSON(api) { if ( symbol.implements ) { symbol.implements.forEach((intf) => { checkSimpleType(intf, `interface of ${symbol.name}`); - let oIntfAPI = defined[intf]; + const oIntfAPI = defined[intf]; if ( oIntfAPI ) { checkClassAgainstInterface(symbol, oIntfAPI); } @@ -3085,6 +2713,7 @@ function createAPIXML(symbols, filename, options = {}) { */ function toRaw(value, type) { if ( typeof value === "string" && !isEnum(type) ) { + // eslint-disable-next-line no-control-regex return "\"" + value.replace(/[\u0000-\u001f"\\]/g, (c) => replacement[c] || "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")) + "\""; } @@ -3229,7 +2858,7 @@ function createAPIXML(symbols, filename, options = {}) { tagWithSince("deprecated", event.deprecated); if ( event.parameters ) { tag("parameters"); - for ( let pn in event.parameters ) { + for ( const pn in event.parameters ) { if ( event.parameters.hasOwnProperty(pn) ) { const param = event.parameters[pn]; @@ -3374,7 +3003,7 @@ function createAPIXML(symbols, filename, options = {}) { let count = 0; if ( props ) { - for (let n in props ) { + for (const n in props ) { if ( props.hasOwnProperty(n) ) { param = props[n]; @@ -3526,7 +3155,7 @@ function createAPIXML(symbols, filename, options = {}) { } if ( !legacyContent ) { - if ( hasSettings && j == 1 && /setting/i.test(param.name) && /object/i.test(param.type) ) { + if ( hasSettings && j === 1 && /setting/i.test(param.name) && /object/i.test(param.type) ) { if ( addRedundancy ) { tag("parameterProperties"); writeParameterPropertiesForMSettings(symbolAPI); diff --git a/lib/processors/jsdoc/lib/ui5/template/utils/typeParser.js b/lib/processors/jsdoc/lib/ui5/template/utils/typeParser.js new file mode 100644 index 000000000..6c72ac5d0 --- /dev/null +++ b/lib/processors/jsdoc/lib/ui5/template/utils/typeParser.js @@ -0,0 +1,489 @@ +/** + * Node script to parse type strings. + * + * (c) Copyright 2009-2024 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +"use strict"; +class ASTBuilder { + literal(str) { + return { + type: "literal", + value: str + }; + } + simpleType(type) { + return { + type: "simpleType", + name: type + }; + } + array(componentType) { + return { + type: "array", + component: componentType + }; + } + object(keyType, valueType) { + return { + type: "object", + key: keyType, + value: valueType + }; + } + set(elementType) { + return { + type: "set", + element: elementType + }; + } + promise(fulfillmentType) { + return { + type: "promise", + fulfill: fulfillmentType + }; + } + "function"(paramTypes, returnType, thisType, constructorType) { + return { + "type": "function", + "params": paramTypes, + "return": returnType, + "this": thisType, + "constructor": constructorType + }; + } + structure(structure) { + return { + type: "structure", + fields: structure + }; + } + union(types) { + return { + type: "union", + types: types + }; + } + synthetic(type) { + type.synthetic = true; + return type; + } + nullable(type) { + type.nullable = true; + return type; + } + mandatory(type) { + type.mandatory = true; + return type; + } + optional(type) { + type.optional = true; + return type; + } + repeatable(type) { + type.repeatable = true; + return type; + } + typeApplication(type, templateTypes) { + return { + type: "typeApplication", + baseType: type, + templateTypes: templateTypes + }; + } +} + +function TypeParser(defaultBuilder = new ASTBuilder()) { + const rLexer = /\s*(Array\.?<|Object\.?<|Set\.?<|Promise\.?<|function\(|\{|:|\(|\||\}|\.?<|>|\)|,|\[\]|\*|\?|!|=|\.\.\.)|\s*(false|true|(?:\+|-)?(?:\d+(?:\.\d+)?|NaN|Infinity)|'[^']*'|"[^"]*"|null|undefined)|\s*((?:module:)?\w+(?:[/.#~][$\w_]+)*)|./g; + + let input; + let builder; + let token; + let tokenStr; + + function next(expected) { + if ( expected !== undefined && token !== expected ) { + throw new SyntaxError( + `TypeParser: expected '${expected}', but found '${tokenStr}' ` + + `(pos: ${rLexer.lastIndex}, input='${input}')` + ); + } + const match = rLexer.exec(input); + if ( match ) { + tokenStr = match[1] || match[2] || match[3]; + token = match[1] || (match[2] && "literal") || (match[3] && "symbol"); + if ( !token ) { + throw new SyntaxError(`TypeParser: unexpected '${match[0]}' (pos: ${match.index}, input='${input}')`); + } + } else { + tokenStr = token = null; + } + } + + function parseType() { + let nullable = false; + let mandatory = false; + if ( token === "?" ) { + next(); + nullable = true; + } else if ( token === "!" ) { + next(); + mandatory = true; + } + + let type; + + if ( token === "literal" ) { + type = builder.literal(tokenStr); + next(); + } else if ( token === "Array.<" || token === "Array<" ) { + next(); + const componentType = parseTypes(); + next(">"); + type = builder.array(componentType); + } else if ( token === "Object.<" || token === "Object<" ) { + next(); + let keyType; + let valueType = parseTypes(); + if ( token === "," ) { + next(); + keyType = valueType; + valueType = parseTypes(); + } else { + keyType = builder.synthetic(builder.simpleType("string")); + } + next(">"); + type = builder.object(keyType, valueType); + } else if ( token === "Set.<" || token === "Set<" ) { + next(); + const elementType = parseTypes(); + next(">"); + type = builder.set(elementType); + } else if ( token === "Promise.<" || token === "Promise<" ) { + next(); + const resultType = parseTypes(); + next(">"); + type = builder.promise(resultType); + } else if ( token === "function(" ) { + next(); + let thisType; + let constructorType; + const paramTypes = []; + let returnType; + if ( tokenStr === "this" ) { + next(); + next(":"); + thisType = parseType(); + if ( token !== ")" ) { + next(","); + } + } else if ( tokenStr === "new" ) { + next(); + next(":"); + constructorType = parseType(); + if ( token !== ")" ) { + next(","); + } + } + while ( token !== ")" ) { + const repeatable = token === "..."; + if ( repeatable ) { + next(); + } + let paramType = parseTypes(); + if ( repeatable ) { + paramType = builder.repeatable(paramType); + } + const optional = token === "="; + if ( optional ) { + paramType = builder.optional(paramType); + next(); + } + paramTypes.push(paramType); + + // exit if there are no more parameters + if ( token !== "," ) { + break; + } + + if ( repeatable ) { + throw new SyntaxError( + `TypeParser: only the last parameter of a function can be repeatable ` + + `(pos: ${rLexer.lastIndex}, input='${input}')` + ); + } + + // consume the comma + next(); + } + next(")"); + if ( token === ":" ) { + next(":"); + returnType = parseType(); + } + type = builder.function(paramTypes, returnType, thisType, constructorType); + } else if ( token === "{" ) { + const structure = Object.create(null); + next(); + do { + const propName = tokenStr; + if ( !/^\w+$/.test(propName) ) { + throw new SyntaxError( + `TypeParser: structure field must have a simple name ` + + `(pos: ${rLexer.lastIndex}, input='${input}', field:'${propName}')` + ); + } + next("symbol"); + let propType; + const optional = token === "="; + if ( optional ) { + next(); + } + if ( token === ":" ) { + next(); + propType = parseTypes(); + } else { + propType = builder.synthetic(builder.simpleType("any")); + } + if ( optional ) { + propType = builder.optional(propType); + } + structure[propName] = propType; + if ( token === "}" ) { + break; + } + next(","); + } while (token); + next("}"); + type = builder.structure(structure); + } else if ( token === "(" ) { + next(); + type = parseTypes(); + next(")"); + } else if ( token === "*" ) { + next(); + type = builder.simpleType("*"); + } else { + type = builder.simpleType(tokenStr); + next("symbol"); + // check for suffix operators: either 'type application' (generics) or 'array', but not both of them + if ( token === "<" || token === ".<" ) { + next(); + const templateTypes = []; + do { + const templateType = parseTypes(); + templateTypes.push(templateType); + if ( token === ">" ) { + break; + } + next(","); + } while (token); + next(">"); + type = builder.typeApplication(type, templateTypes); + } else { + while ( token === "[]" ) { + next(); + type = builder.array(type); + } + } + } + if ( builder.normalizeType ) { + type = builder.normalizeType(type); + } + if ( nullable ) { + type = builder.nullable(type); + } + if ( mandatory ) { + type = builder.mandatory(type); + } + return type; + } + + function parseTypes() { + const types = []; + do { + types.push(parseType()); + if ( token !== "|" ) { + break; + } + next(); + } while (token); + return types.length === 1 ? types[0] : builder.union(types); + } + + this.parse = function(typeStr, tempBuilder = defaultBuilder) { + /* + try { + const r = catharsis.parse(typeStr, { jsdoc: true}); + console.log(JSON.stringify(typeStr, null, "\t"), r); + } catch (err) { + console.log(typeStr, err); + } + */ + builder = tempBuilder; + input = String(typeStr); + rLexer.lastIndex = 0; + next(); + const type = parseTypes(); + next(null); + return type; + }; + + /** + * Parses a string representing a complex type and returns an object with 2 fields: + * (1) simpleTypes: an array of the identified simple types inside the complex type; + * (2) template: a string indicating the position of the simple types in the original string. + * + * Examples: + * + * parseSimpleTypes("sap.ui.core.Control | null") returns + * { + * template: "${0} | ${1}", + * simpleTypes: ["sap.ui.core.Control", "null"] + * } + * + * parseSimpleTypes("Array|Array") returns + * { + * template: "Array<${0}>|Array<${1}>" + * simpleTypes: ["string", "number"], + * } + * + * parseSimpleTypes("Object") returns + * { + * template: "Object<${0},${1}>" + * simpleTypes: ["string", "number"], + * } + * + * parseSimpleTypes("function(sap.ui.base.Event, number): boolean") returns + * { + * template: "function(${0},${1}): ${2}" + * simpleTypes: ["sap.ui.base.Event", "number", "boolean"], + * } + * + * parseSimpleTypes("Promise") returns + * { + * template: "Promise<${0}>" + * simpleTypes: ["string"], + * } + * + * @param {string} sComplexType + * @param {function} [fnFilter] optional filter function to be called for each simple type found. If a type is filtered out, it will not be added to the list of simple types, but will be present in its original form in the template. + * @returns {{simpleTypes: string[], template: string}} an object with the properties template and simpleTypes + */ + this.parseSimpleTypes = function(sComplexType, fnFilter) { + const parsed = this.parse(sComplexType , new ASTBuilder() ); + let iIndexOfNextSimpleType = 0; + + function processSimpleType(sType) { + var bSkip = fnFilter && !fnFilter(sType); + if (bSkip) { + return { + template: sType, + simpleTypes: [] // do not add this type to the list of parsed types + }; + } + + return { + template: "${" + iIndexOfNextSimpleType++ + "}", + simpleTypes: [sType] // add this type to the list of parsed types + }; + } + + function findSimpleTypes(parsed) { + + /* eslint-disable no-case-declarations */ + switch (parsed.type) { + case "simpleType": + return processSimpleType(parsed.name); + case "literal": + return processSimpleType(parsed.value); + case "array": + const component = findSimpleTypes(parsed.component); + return { + template: "Array<" + component.template + ">", + simpleTypes: component.simpleTypes + }; + case "object": + const key = findSimpleTypes(parsed.key); + const value = findSimpleTypes(parsed.value); + return { + template: "Object<" + key.template + "," + value.template + ">", + simpleTypes: key.simpleTypes.concat(value.simpleTypes) + }; + case "function": + const aParamTemplates = []; + let aParamsimpleTypes = []; + parsed.params.forEach(function(paramType) { + const types = findSimpleTypes(paramType); + aParamTemplates.push(types.template); + aParamsimpleTypes = aParamsimpleTypes.concat(types.simpleTypes); + }); + const returnType = parsed.return ? findSimpleTypes(parsed.return) : {simpleTypes: []}; + const returnTemplate = returnType.template ? " : " + returnType.template : ""; + const finalTemplate = "function(" + aParamTemplates.join(",") + ")" + returnTemplate; + return { + template: finalTemplate, + simpleTypes: aParamsimpleTypes.concat(returnType.simpleTypes) + }; + case "union": + const unionParts = parsed.types, aPartsTemplates = []; + let aPartsSimpleTypes = []; + unionParts.forEach(function (part) { + const types = findSimpleTypes(part); + aPartsTemplates.push(types.template); + aPartsSimpleTypes = aPartsSimpleTypes.concat(types.simpleTypes); + }); + return { + template: aPartsTemplates.join(" | "), + simpleTypes: aPartsSimpleTypes + }; + case "promise": + const fulfill = findSimpleTypes(parsed.fulfill); + return { + template: "Promise<" + fulfill.template + ">", + simpleTypes: fulfill.simpleTypes + }; + case "set": + const element = findSimpleTypes(parsed.element); + return { + template: "Set<" + element.template + ">", + simpleTypes: element.simpleTypes + }; + case "typeApplication": + const baseType = findSimpleTypes(parsed.baseType); + const templateTypes = parsed.templateTypes.map(findSimpleTypes); + return { + template: baseType.template + "<" + templateTypes.map(function (type) { + return type.template; + }).join(",") + ">", + simpleTypes: baseType.simpleTypes.concat(templateTypes.reduce(function (a, b) { + return a.concat(b.simpleTypes); + }, [])) + }; + case "structure": + const aFields = []; + let aSimpleTypes = []; + Object.keys(parsed.fields).forEach(function (sKey) { + const oField = parsed.fields[sKey]; + const types = findSimpleTypes(oField); + aFields.push(sKey + ":" + types.template); + aSimpleTypes = aSimpleTypes.concat(types.simpleTypes); + }); + return { + template: "{" + aFields.join(",") + "}", + simpleTypes: aSimpleTypes + }; + } + /* eslint-enable no-case-declarations */ + } + + return findSimpleTypes(parsed); + }; +} + + +module.exports = { + ASTBuilder, + TypeParser +}; diff --git a/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js b/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js index da08f0057..ede73a095 100644 --- a/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js +++ b/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js @@ -1,4 +1,4 @@ - +"use strict"; const rSinceVersion = /^([0-9]+(?:\.[0-9]+(?:\.[0-9]+)?)?([-.][0-9A-Z]+)?)(\.$|\.\s+|[,:;]\s*|\s-\s*|\s|$)/i; function _parseVersion(value) { @@ -11,8 +11,9 @@ function _parseVersion(value) { version: m[1], versionFollowedBySpace: versionFollowedBySpace, nextPosition: m[0].length - } + }; } + return undefined; } /** @@ -38,9 +39,10 @@ function extractVersion(value) { if (parseResult && parseResult.versionFollowedBySpace) { return parseResult.version; } + return undefined; } -const rSinceIndicator = /^(?:as\s+of|since)(?:\s+version)?\s*/i +const rSinceIndicator = /^(?:as\s+of|since)(?:\s+version)?\s*/i; /** * Extracts since information from given value. @@ -115,4 +117,4 @@ function extractSince(value) { module.exports = { extractSince, extractVersion -} \ No newline at end of file +};