From fb56484e3a82f0872a9e6c72d77fadb171d451c7 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:19:53 +0300 Subject: [PATCH 1/9] refactor: JSDoc: ensure that validation can detect use of @experimental Change-Id: Ic5ea014fe02aeebd1928caceb0a34497756c22e8 Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/bb1f0286282b12f93963e3ad8e9fc3acdde35306 --- packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js | 4 ++-- .../builder/lib/processors/jsdoc/lib/ui5/template/publish.js | 2 +- .../processors/jsdoc/lib/ui5/template/utils/versionUtil.js | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js index e7e5f07901b..10c15758543 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js @@ -2691,7 +2691,7 @@ exports.defineTags = function(dictionary) { mustHaveValue: true, onTagged: function(doclet, tag) { if (isSemVer(doclet.experimental)) { - error(`@since and @ui5-experimental-since must not be used both for the same API (${doclet.meta?.lineno})`); + warning(`@since and @ui5-experimental-since must not be used both for the same API (${doclet.meta?.lineno})`); } doclet.since = tag.value; } @@ -2743,7 +2743,7 @@ exports.defineTags = function(dictionary) { mustHaveValue: true, onTagged: function(doclet, tag) { if (doclet.since || doclet.experimental) { - error(`@experimental/@since and @ui5-experimental-since must not be used together for the same API (${doclet.meta?.lineno})`); + warning(`@experimental/@since and @ui5-experimental-since must not be used together for the same API (${doclet.meta?.lineno})`); } const version = tag.value?.trim() ?? ""; if (!isSemVer(version)) { diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js index 98ad45f43ee..698c005c188 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -1084,7 +1084,7 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { future(`**** Failed to parse version in string '${value}'. ` + `Version might be missing or has an unexpected format.`); } - if ( info.value ) { + if ( info.value || (info.value === "" && name === "experimental")) { curr["text"] = normalizeWS(info.value); } closeTag(name, true); diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js b/packages/builder/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js index 8623a230b69..5a6644928c2 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js @@ -78,10 +78,11 @@ const rSinceIndicator = /^(?:as\s+of|since)(?:\s+version)?\s*/i; * * * @param {string|boolean} value + * @param {boolean} [keepEmptyText=false] * @returns {{pos: number, value: string, since: string|null|undefined}} * undefined: value is falsy */ -function extractSince(value) { +function extractSince(value, keepEmptyText = false) { if ( !value ) { return undefined; @@ -97,7 +98,7 @@ function extractSince(value) { return { since: value, pos: value.length, - value: "" + value: keepEmptyText ? "" : null }; } From d58ce615cda99adb27bd132c0e7b71ba4c280af3 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:19:54 +0300 Subject: [PATCH 2/9] refactor: JSDoc: support multiline default values in @param tags JSDoc's own type parser (jsdoc.tag.type.parse) is also used to split the name portion of a parameter into name and default value. The RegEx used for that purpose is missing the multiline (/m) flag and therefore does not accept line feeds within the default value. As this is encapsulated in a closure within the type parser, it can only be fixed via a post-processing step. Change-Id: I4acb08ae88cbc2255033fcfc13e4d7d66c9fcadc Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/09587907eab365b70ba2256c1d9cdc78fbd48739 --- .../lib/processors/jsdoc/lib/ui5/plugin.js | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js index 10c15758543..cab13e81654 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js @@ -2691,7 +2691,7 @@ exports.defineTags = function(dictionary) { mustHaveValue: true, onTagged: function(doclet, tag) { if (isSemVer(doclet.experimental)) { - warning(`@since and @ui5-experimental-since must not be used both for the same API (${doclet.meta?.lineno})`); + error(`@since and @ui5-experimental-since must not be used both for the same API (${doclet.meta?.lineno})`); } doclet.since = tag.value; } @@ -2743,7 +2743,7 @@ exports.defineTags = function(dictionary) { mustHaveValue: true, onTagged: function(doclet, tag) { if (doclet.since || doclet.experimental) { - warning(`@experimental/@since and @ui5-experimental-since must not be used together for the same API (${doclet.meta?.lineno})`); + error(`@experimental/@since and @ui5-experimental-since must not be used together for the same API (${doclet.meta?.lineno})`); } const version = tag.value?.trim() ?? ""; if (!isSemVer(version)) { @@ -3551,6 +3551,23 @@ exports.astNodeVisitor = { tagInfo.type = getTypeStrings(tagInfo.parsedType); // console.info("new parse result", tagInfo.type); } + // #### BEGIN: MODIFIED BY SAP + if (tagInfo && tagInfo.name && /^\[|=/.test(tagInfo.name)) { + // debug("tag info before post-processing the name-portion", tagInfo); + let name = tagInfo.name; + if (name.startsWith("[") && name.endsWith("]")) { + name = name.slice(1,-1).trim(); + tagInfo.optional = true; + } + const p = name.indexOf("="); + if (p > 0) { // intentionaly checks for != 0 (no empty name allowed) + tagInfo.defaultvalue = name.slice(p + 1).trim(); + name = name.slice(0, p).trim(); + } + tagInfo.name = name; + // debug("tag info after post-processing the name-portion", tagInfo); + } + // #### END: MODIFIED BY SAP return tagInfo; }; }()); From e68fa86471785d21d68cd937dd74c3fc72a5f900 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:19:54 +0300 Subject: [PATCH 3/9] refactor: JSDoc: allow to mark event parameters as optional A parameter of a managed event (an event with metadata in a subclass of ManagedObject) can be flagged as optional by appending a question mark ("?") to its name. Example: metadata { events: { change: { value: "any", // mandatory "oldValue?": "any" // optional } } } The generated documentation for the managed event reflects the optional state of a param. Change-Id: If0781a73a8160710b1be09e444c46feb6d3230c6 Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/03dcf95dfe2cbcc174bccbb918dd394ba94f5a85 --- .../lib/processors/jsdoc/lib/ui5/plugin.js | 16 ++++++++++++---- .../processors/jsdoc/lib/ui5/template/publish.js | 6 ++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js index cab13e81654..84a316b707b 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js @@ -1480,13 +1480,18 @@ function collectClassInfo(extendCall, classDoclet) { } }; each(settings.parameters, "type", (pName, pSettings, pDoclet) => { + const optional = pName.endsWith("?"); + if (optional) { + pName = pName.slice(0, -1); + } info.parameters[pName] = { name : pName, doc : pDoclet && pDoclet.description, deprecation : pDoclet && pDoclet.deprecated, since : pDoclet && pDoclet.since, experimental : pDoclet && pDoclet.experimental, - type : pSettings && pSettings.type ? pSettings.type.value.value : "" + type : pSettings && pSettings.type ? pSettings.type.value.value : "", + optional }; }); }); @@ -2353,8 +2358,11 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, "@param {object} oControlEvent.getParameters" ]; for (const pName in info.parameters ) { + const optional = info.parameters[pName].optional; lines.push( - "@param {" + (info.parameters[pName].type || "") + "} oControlEvent.getParameters." + pName + " " + (info.parameters[pName].doc || "") + "@param {" + (info.parameters[pName].type || "") + "} " + + (optional ? "[" : "") + "oControlEvent.getParameters." + pName + (optional ? "]" : "") + + " " + (info.parameters[pName].doc || "") ); } lines.push(visibilityTags); @@ -2691,7 +2699,7 @@ exports.defineTags = function(dictionary) { mustHaveValue: true, onTagged: function(doclet, tag) { if (isSemVer(doclet.experimental)) { - error(`@since and @ui5-experimental-since must not be used both for the same API (${doclet.meta?.lineno})`); + warning(`@since and @ui5-experimental-since must not be used both for the same API (${doclet.meta?.lineno})`); } doclet.since = tag.value; } @@ -2743,7 +2751,7 @@ exports.defineTags = function(dictionary) { mustHaveValue: true, onTagged: function(doclet, tag) { if (doclet.since || doclet.experimental) { - error(`@experimental/@since and @ui5-experimental-since must not be used together for the same API (${doclet.meta?.lineno})`); + warning(`@experimental/@since and @ui5-experimental-since must not be used together for the same API (${doclet.meta?.lineno})`); } const version = tag.value?.trim() ?? ""; if (!isSemVer(version)) { diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js index 698c005c188..c1efe78d59d 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -1394,6 +1394,9 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { tag(pn); attrib("name", pn); attrib("type", param.type); + if (param.optional) { + attrib("optional", !!param.optional, false, /* raw = */true); + } attribSince(param.since); tag("description", normalizeWS(param.doc), true); tagWithSince("experimental", param.experimental); @@ -1918,6 +1921,9 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { tag("parameter"); attrib("name", param.name); attrib("type", listTypes(param.type)); + if (param.optional) { + attrib("optional", param.optional, false, /* raw = */ true); + } if ( param.since ) { attrib("since", extractVersion(param.since)); } From c927cdb066d1e23292e41a6dc0a82d97e4fb8b4a Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:20:03 +0300 Subject: [PATCH 4/9] refactor: JSDoc: generated docs should honor module overrides When a class documentation contains a module override, then the generated method documentation should use the same override. This prevents misleading module information in the api.json. Change-Id: Ic194de0ad717518e22d3313c5d5bb4f39934d046 Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/07a0689d32f3d7d4022326741c1f763195967bab --- packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js index 84a316b707b..5a6f1c0184b 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js @@ -1793,7 +1793,10 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, lines = Array.prototype.concat.apply([], lines); // flatten lines = removeDuplicateEmptyLines(lines); lines.push("@synthetic"); - + if (doclet?.__ui5?.moduleOverride) { + const [moduleName, exportName] = doclet.__ui5.moduleOverride; + lines.push(`@ui5-module-override ${moduleName} ${exportName}`.trim()); + } const comment = " * " + lines.join("\r\n * "); jsdocCommentFound("/**\r\n" + comment + "\r\n */"); From 9ac6d648ceb6219ae0003d0d7a19b6a26e98e063 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:20:03 +0300 Subject: [PATCH 5/9] refactor: JSDoc: Enrich param/property with since, experimental, deprecated info Add support for {@since}, {@ui5-experimental-since} and {@deprecated} inline tags inside @param and @property (typedef) descriptions. The JSDoc plugin parses these inline tags and stores the metadata on the doclet's params/properties arrays. The publish template forwards the data for typedef properties. The transformApiJson processor formats deprecatedText and experimentalText fields. The SubApiDetail view renders MessageStrips for parameters and typedef fields based on these formatted fields. JIRA: BGSOFUIPIRIN-7056 Change-Id: I90ecb699b2065c004af8662d21f6ffa1a570bd44 Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/e892fb4a18fc7ab2a1a90549aa4b7042a9630b48 --- .../processors/jsdoc/lib/transformApiJson.js | 107 ++++++++++++++--- .../lib/processors/jsdoc/lib/ui5/plugin.js | 108 +++++++++++++++++- .../jsdoc/lib/ui5/template/publish.js | 9 +- 3 files changed, 202 insertions(+), 22 deletions(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js b/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js index 6b9b9286797..cef99bc88b2 100644 --- a/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js +++ b/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js @@ -9,6 +9,7 @@ const cheerio = require("cheerio"); const path = require('path'); const {TypeParser} = require("./ui5/template/utils/typeParser"); +const {extractSince} = require("./ui5/template/utils/versionUtil"); const log = (function() { try { return require("@ui5/logger").getLogger("builder:processors:jsdoc:transformApiJson"); @@ -455,8 +456,20 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oParameter.defaultValue = formatters.formatDefaultValue(oParameter.defaultValue); // Description - if (oParameter.description) { - oParameter.description = formatters.formatDescription(oParameter.description); + oParameter.description = formatters.formatDescriptionSince(oParameter.description, oParameter.since); + oParameter.description = formatters.formatDescription(oParameter.description); + + // Experimental + if (oParameter.experimental) { + oParameter.experimentalText = formatters.formatExperimental(oParameter.experimental.since ?? oParameter.experimental); + } + + // Deprecated + if (oParameter.deprecated) { + const dep = typeof oParameter.deprecated === "string" + ? extractSince(oParameter.deprecated) + : { since: oParameter.deprecated.since, value: oParameter.deprecated.text }; + oParameter.deprecatedText = formatters.formatDeprecated(dep?.since, dep?.value); } }); @@ -506,11 +519,20 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oProperty.name = formatters.formatEntityName(oProperty.name, oSymbol.name, oProperty.static); // Description + oProperty.description = formatters.formatDescriptionSince(oProperty.description, oProperty.since); + oProperty.description = formatters.formatDescription(oProperty.description); + + // Experimental + if (oProperty.experimental) { + oProperty.experimentalText = formatters.formatExperimental(oProperty.experimental.since ?? oProperty.experimental); + } + + // Deprecated if (oProperty.deprecated) { - oProperty.description = formatters.formatDescription(oProperty.description, - oProperty.deprecated.text, oProperty.deprecated.since); - } else { - oProperty.description = formatters.formatDescription(oProperty.description); + const dep = typeof oProperty.deprecated === "string" + ? extractSince(oProperty.deprecated) + : { since: oProperty.deprecated.since, value: oProperty.deprecated.text }; + oProperty.deprecatedText = formatters.formatDeprecated(dep?.since, dep?.value); } // Examples @@ -582,6 +604,11 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Default value oProperty.defaultValue = formatters.formatDefaultValue(oProperty.defaultValue); + // Experimental + if (oProperty.experimental) { + oProperty.experimentalText = formatters.formatExperimental(oProperty.experimental.since ?? oProperty.experimental); + } + // Deprecated if (oProperty.deprecated) { oProperty.deprecatedText = formatters.formatDeprecated(oProperty.deprecated.since, @@ -622,6 +649,11 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oAggregation.description = formatters.formatDescriptionSince(oAggregation.description, oAggregation.since); } + // Experimental + if (oAggregation.experimental) { + oAggregation.experimentalText = formatters.formatExperimental(oAggregation.experimental.since ?? oAggregation.experimental); + } + // Link enabled oAggregation.linkEnabled = !isBuiltInType(oAggregation.type); }); @@ -649,11 +681,18 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, } // Description + oAssociation.description = formatters.formatDescriptionSince(oAssociation.description, oAssociation.since); + oAssociation.description = formatters.formatDescription(oAssociation.description); + + // Experimental + if (oAssociation.experimental) { + oAssociation.experimentalText = formatters.formatExperimental(oAssociation.experimental.since ?? oAssociation.experimental); + } + + // Deprecated if (oAssociation.deprecated) { - oAssociation.description = formatters.formatDescription(oAssociation.description, - oAssociation.deprecated.text, oAssociation.deprecated.since); - } else { - oAssociation.description = formatters.formatDescriptionSince(oAssociation.description, oAssociation.since); + oAssociation.deprecatedText = formatters.formatDeprecated(oAssociation.deprecated.since, + oAssociation.deprecated.text); } }); } @@ -784,10 +823,17 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oParameter.description = formatters.formatDescription(oParameter.description); } + // Experimental + if (oParameter.experimental) { + oParameter.experimentalText = formatters.formatExperimental(oParameter.experimental.since ?? oParameter.experimental); + } + // Deprecated if (oParameter.deprecated) { - oParameter.deprecatedText = formatters.formatDeprecated(oParameter.deprecated.since, - oParameter.deprecated.text); + const dep = typeof oParameter.deprecated === "string" + ? extractSince(oParameter.deprecated) + : { since: oParameter.deprecated.since, value: oParameter.deprecated.text }; + oParameter.deprecatedText = formatters.formatDeprecated(dep?.since, dep?.value); } }); } @@ -827,7 +873,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Deprecated if (oMethod.deprecated) { - oMethod.deprecatedText = formatters.formatEventDeprecated(oMethod.deprecated.since, + oMethod.deprecatedText = formatters.formatMethodDeprecated(oMethod.deprecated.since, oMethod.deprecated.text); } @@ -842,11 +888,20 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oParameter.defaultValue = formatters.formatDefaultValue(oParameter.defaultValue); // Description + oParameter.description = formatters.formatDescriptionSince(oParameter.description, oParameter.since); + oParameter.description = formatters.formatDescription(oParameter.description); + + // Experimental + if (oParameter.experimental) { + oParameter.experimentalText = formatters.formatExperimental(oParameter.experimental.since ?? oParameter.experimental); + } + + // Deprecated if (oParameter.deprecated) { - oParameter.description = formatters.formatDescription(oParameter.description, - oParameter.deprecated.text, oParameter.deprecated.since); - } else { - oParameter.description = formatters.formatDescription(oParameter.description); + const dep = typeof oParameter.deprecated === "string" + ? extractSince(oParameter.deprecated) + : { since: oParameter.deprecated.since, value: oParameter.deprecated.text }; + oParameter.deprecatedText = formatters.formatDeprecated(dep?.since, dep?.value); } }); @@ -1490,6 +1545,10 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, return result; }, + formatExperimental: function (sExperimentalSince) { + return `Experimental since ${sExperimentalSince}`; + }, + /** * Formats the default value of the property as a string. * @param defaultValue - the default value of the property @@ -2414,9 +2473,21 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Phone name - available only for parameters oProperty.phoneName = [aPhoneName.join("."), oProperty.name].join("."); - // Description (no deprecation etc. possible in JSDoc) + // Description + oProperty.description = formatters.formatDescriptionSince(oProperty.description, oProperty.since); oProperty.description = formatters.formatDescription(oProperty.description); + // Experimental + if (oProperty.experimental) { + oProperty.experimentalText = formatters.formatExperimental(oProperty.experimental.since ?? oProperty.experimental); + } + + // Deprecated + if (oProperty.deprecated) { + oProperty.deprecatedText = formatters.formatDeprecated(oProperty.deprecated.since, + oProperty.deprecated.text); + } + // Add property to parameter array as we need a simple structure aProperties.push(oProperty); diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js index 5a6f1c0184b..180e7de3161 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js @@ -74,7 +74,8 @@ const logger = require('jsdoc/util/logger'); const pluginConfig = (env.conf && env.conf.templates && env.conf.templates.ui5) || env.opts.sapui5 || {}; const escope = require("escope"); const templatePath = path.getResourcePath(env.opts.template) || "./template"; -const {isSemVer} = require(`${templatePath}/utils/versionUtil`); +const {isSemVer, extractSince} = require(`${templatePath}/utils/versionUtil`); +const inline = require("jsdoc/tag/inline"); /* ---- logging ---- */ @@ -2943,6 +2944,77 @@ exports.defineTags = function(dictionary) { }); }; +const inlineTags = { + "ui5-experimental-since": { + onTagged(doclet, symbol, tag) { + if (symbol.since || symbol.experimental) { + error(`@experimental/@since and @ui5-experimental-since must not be used together for the same API entity (line:${doclet.meta?.lineno})`); + } + const version = tag.value?.trim() ?? ""; + if (!isSemVer(version)) { + error(`@ui5-experimental-since must have a version and only a version, not '${version}' (line:${doclet.meta?.lineno})`); + } + symbol.experimental = version; + } + }, + "since": { + onTagged(doclet, symbol, tag) { + if (isSemVer(symbol.experimental)) { + error(`@since and @ui5-experimental-since must not be used both for the same API (line:${doclet.meta?.lineno})`); + } + if (!isSemVer(tag.value)) { + error(`@since must have a version and only a version, not '${tag.value}' (line:${doclet.meta?.lineno})`); + } + symbol.since = tag.value; + } + }, + "deprecated": { + onTagged(doclet, symbol, tag) { + const info = extractSince(tag.value); + if (!info?.since) { + error(`@deprecated inline tag must include a version (line:${doclet.meta?.lineno})`); + } + if (!info?.value) { + error(`@deprecated inline tag must include a description (line:${doclet.meta?.lineno})`); + } + symbol.deprecated = tag.value; // raw string, parsed again in transformApiJson + } + } +}; + +function fixAndRemoveTagInfo(str, tagInfo) { + if (tagInfo.text.includes("{@")) { + const startTag = str.indexOf(tagInfo.completeTag); + if (startTag === -1) { + return str.replace(tagInfo.completeTag, ""); + } + const startText = str.indexOf(tagInfo.text, startTag); + let openBraces = 1; + let endTag = startText; + while (endTag < str.length) { + const c = str.charAt(endTag++); + if (c === "{") { + openBraces++; + } else if (c === "}") { + openBraces--; + if (openBraces === 0) { + break; + } + } + } + const endText = openBraces === 0 ? endTag - 1 : endTag; + tagInfo.completeTag = str.slice(startTag, endTag); + tagInfo.text = str.slice(startText, endText).trim(); + } + return str.replace(tagInfo.completeTag, ""); +} + +const inlineTagReplacers = Object.keys(inlineTags).reduce( + (result, key) => { + result[key] = fixAndRemoveTagInfo; + return result; + }, {}); + exports.handlers = { /** @@ -3064,6 +3136,40 @@ exports.handlers = { warning(`fixing return type 'this' with ${e.doclet.memberof}`); e.doclet.returns[0].type.names[0] = e.doclet.memberof; }*/ + + // process UI5 inline tags in param tags + e.doclet.params?.forEach((param) => { + if (param.description) { + const result = inline.replaceInlineTags(param.description, inlineTagReplacers); + if (result.tags.length > 0) { + param.description = result.newString; + result.tags.forEach((tagInfo) => { + const tag = { + title: tagInfo.tag, + value: tagInfo.text + }; + inlineTags[tagInfo.tag].onTagged(e.doclet, param, tag); + }); + } + } + }); + + // process UI5 inline tags in @property tags (typedef) + e.doclet.properties?.forEach((prop) => { + if (prop.description) { + const result = inline.replaceInlineTags(prop.description, inlineTagReplacers); + if (result.tags.length > 0) { + prop.description = result.newString; + result.tags.forEach((tagInfo) => { + const tag = { + title: tagInfo.tag, + value: tagInfo.text + }; + inlineTags[tagInfo.tag].onTagged(e.doclet, prop, tag); + }); + } + } + }); }, beforeParse : function(e) { diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js index c1efe78d59d..20bc4ad3ee2 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -1534,9 +1534,9 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { stakeholderList("allowedFor", stakeholders(symbol)); } - // as JSDoc does not allow tags for other tags, no @deprecated etc. can be supported - // tagWithSince("experimental", properties[i].experimental); - // tagWithSince("deprecated", properties[i].deprecated); + attribSince(properties[i].since); + tagWithSince("experimental", properties[i].experimental); + tagWithSince("deprecated", properties[i].deprecated); closeTag(name); } @@ -1841,6 +1841,9 @@ function createAPIJSON4Symbol(symbol, omitDefaults) { stakeholderList("allowedFor", stakeholders(symbol)); } tag("description", normalizeWS(prop.description), true); + attribSince(prop.since); + tagWithSince("experimental", prop.experimental); + tagWithSince("deprecated", prop.deprecated); closeTag("property"); }); endCollection("properties"); From 698f3e209bdf29cd709ac00b6135a5c872a09c4b Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:20:04 +0300 Subject: [PATCH 6/9] refactor: DemoKit: Remove duplicate @since information for properties in API documentation Background: The @since information for typedef and enum properties was displayed twice in the API reference. The transformApiJson process appended the @since value to the property description in italics (the original mechanism, added before a dedicated since field existed in api.json). Meanwhile the api.json now also exposes a dedicated "since" attribute, which the SubApiDetail view renders as its own "Since" ObjectStatus below the visibility information. As a result the same version appeared both italicized in the description and as the dedicated status. Additionally, when a parameter typed as a typedef was expanded inline, the imperatively built sub-rows did not render the @since information at all, although the data was available on the property. Solution: - transformApiJson no longer appends @since to the description for typedef/enum properties (both the top-level properties loop and the nested buildPropertiesModel builder). The dedicated "Since" ObjectStatus is now the single source of truth. Constructor/method/event parameters, control-metadata properties, aggregations and associations are left unchanged, as their tables have no dedicated Since status. - _addTypedefSubRows now renders a "Since" ObjectStatus for expanded typedef properties, matching the dedicated typedef Fields table. JIRA: BGSOFUIPIRIN-7093 Change-Id: I6f24918111c9268a8ae19e30ed4cd9460b06ace7 Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/e5e3f87243f1deaadeb2d7af927809a3b66b6965 --- .../builder/lib/processors/jsdoc/lib/transformApiJson.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js b/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js index cef99bc88b2..f89983554e2 100644 --- a/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js +++ b/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js @@ -519,7 +519,9 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oProperty.name = formatters.formatEntityName(oProperty.name, oSymbol.name, oProperty.static); // Description - oProperty.description = formatters.formatDescriptionSince(oProperty.description, oProperty.since); + // Note: The "@since" information is not appended to the description here because it is + // rendered separately as a dedicated "Since" status in the SubApiDetail view. Appending it + // would duplicate the version information for typedef/enum properties. oProperty.description = formatters.formatDescription(oProperty.description); // Experimental @@ -2474,7 +2476,9 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oProperty.phoneName = [aPhoneName.join("."), oProperty.name].join("."); // Description - oProperty.description = formatters.formatDescriptionSince(oProperty.description, oProperty.since); + // Note: The "@since" information is not appended to the description here because it is + // rendered separately as a dedicated "Since" status in the SubApiDetail view. Appending it + // would duplicate the version information for typedef/enum properties. oProperty.description = formatters.formatDescription(oProperty.description); // Experimental From 0d98f9c8ff55d661d4980b3ec5a9702b42227ade Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:20:16 +0300 Subject: [PATCH 7/9] refactor: JSDoc: support enum values whose name is not an identifier Change-Id: Id89f58c5bfbb33dd3bca35fe71c89c90be93e3a1 Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/9d84b5361271894f7ceab6e8c2b4eb24760a3538 --- packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js index 180e7de3161..59bcfcff6cd 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js @@ -76,6 +76,7 @@ const escope = require("escope"); const templatePath = path.getResourcePath(env.opts.template) || "./template"; const {isSemVer, extractSince} = require(`${templatePath}/utils/versionUtil`); const inline = require("jsdoc/tag/inline"); +const getASTNodeInfo = require("jsdoc/src/astnode").getInfo; /* ---- logging ---- */ @@ -3402,7 +3403,11 @@ exports.astNodeVisitor = { function processPotentialEnum(literal, comment) { const values = literal.properties.reduce((map, prop) => { - map[getPropertyKey(prop)] = convertValue(prop.value); + // use the JSDoc API to determine a name from the property's key + // this is necessary to get the same enumID as in the later step where + // name info from a JSDoc doclet is used to build the enumID + const key = getASTNodeInfo(prop).name; + map[key] = convertValue(prop.value); return map; }, Object.create(null)); // determine unique enum ID from key set From d3a739b3b09c55a9745bcf18355951fe70a84613 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:20:16 +0300 Subject: [PATCH 8/9] refactor: JSDoc: reduce log messages - report use of deprecated @experimental tag only for non-deprecated APIs - reduce log level for message reg. automatically added @lends tag - add more native ES and Web APIs that are meanwhile referenced in our JSDoc - fix some typos in runtime sources, add missing JSDoc Change-Id: I4db700a7157ce5c108855b1480ebd3cde6c02907 Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/f0c50cf352fe208532449983f1b3bf479ab3d3a9 --- packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js | 9 +++++++-- .../lib/processors/jsdoc/lib/ui5/template/publish.js | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js index 59bcfcff6cd..b2279dfd8fb 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/plugin.js @@ -1297,7 +1297,7 @@ function collectClassInfo(extendCall, classDoclet) { if (classInfoComment == null) { // If the class info object doesn't have a @lends tag, add it const raw = `/** @lends ${jsdocClassName}.prototype */`; - info(`lends comment injected for class '${jsdocClassName}'`); + debug(`lends comment injected for class '${jsdocClassName}'`); addLeadingCommentNode(classInfoNode, { type: "CommentBlock", raw, @@ -2730,7 +2730,6 @@ exports.defineTags = function(dictionary) { dictionary.defineTag('experimental', { // value is optional onTagged: function(doclet, tag) { - future(`As of version 1.139, the experimental tag is deprecated and should no longer be used. Use the @ui5-experimental-since tag instead (${doclet.meta?.lineno})`); if (isSemVer(doclet.experimental)) { error(`@experimental and @ui5-experimental-since must not be used both for the same API (${doclet.meta?.lineno})`); } @@ -3138,6 +3137,12 @@ exports.handlers = { e.doclet.returns[0].type.names[0] = e.doclet.memberof; }*/ + if (e.doclet.experimental + && !e.doclet.deprecated + && !isSemVer(e.doclet.experimental)) { + future(`As of version 1.139, the experimental tag is deprecated and should no longer be used. Use the @ui5-experimental-since tag instead (${e.doclet.meta?.lineno})`); + } + // process UI5 inline tags in param tags e.doclet.params?.forEach((param) => { if (param.description) { diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js index 20bc4ad3ee2..d9447829451 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -2266,6 +2266,8 @@ const builtinTypes = { Boolean:true, Date:true, Error:true, + GeneratorFunction: true, + Iterator: true, Map:true, Number:true, String:true, @@ -2297,11 +2299,14 @@ const builtinTypes = { HTMLElement:true, Node:true, PerformanceResourceTiming:true, + ReadableStream: true, Request:true, Response:true, Storage:true, Touch:true, TouchList:true, + URL: true, + URLSearchParams: true, Window:true, XMLDocument:true From 664e906ca37008201807ae8fd248949d26ae754d Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 21 Oct 2026 16:20:17 +0300 Subject: [PATCH 9/9] refactor: JSDoc: Improve empty JSON handling JSON.parse over an empty string throws an exceptiom without providing meaningful context over the failure. Improve error messages, so that it's clear where the exception originates from. JIRA: CPOUI5FOUNDATION-1046 Change-Id: I8a79c382810f7660fd39c1a230274b59cf94f3db Ported from SAP/openui5 commit: https://github.com/SAP/openui5/commit/dbecbea3ee735284e06d81480674d333fe2b202c --- .../processors/jsdoc/lib/createIndexFiles.js | 6 ++- .../processors/jsdoc/lib/transformApiJson.js | 42 ++++++++++++++----- .../jsdoc/lib/ui5/template/publish.js | 3 ++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/builder/lib/processors/jsdoc/lib/createIndexFiles.js b/packages/builder/lib/processors/jsdoc/lib/createIndexFiles.js index 8dd7bf42e78..b11afedb140 100644 --- a/packages/builder/lib/processors/jsdoc/lib/createIndexFiles.js +++ b/packages/builder/lib/processors/jsdoc/lib/createIndexFiles.js @@ -67,7 +67,11 @@ function createIndexFiles(versionInfoFile, unpackedTestresourcesRoot, targetFile // Handle empty files scenario resolve({}); } else { - resolve(JSON.parse(String(data))); + try { + resolve(JSON.parse(String(data))); + } catch (oParseError) { + reject(new SyntaxError(`Failed to parse "${file}" as JSON: ${oParseError.message}`)); + } } }); }); diff --git a/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js b/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js index f89983554e2..c80be271665 100644 --- a/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js +++ b/packages/builder/lib/processors/jsdoc/lib/transformApiJson.js @@ -1033,21 +1033,29 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oChainObject.aDependentLibraryFiles.forEach((sFile) => { aPromises.push(new Promise((oResolve) => { fs.readFile(sFile, 'utf8', (oError, oData) => { - oResolve(oError ? false : oData); + oResolve(oError ? false : {sFile, sData: oData}); }); })); }); return Promise.all(aPromises).then((aValues) => { const oDependentAPIs = {}; - aValues.forEach((sData) => { + aValues.forEach((oValue) => { + if (!oValue) { + return; + } + const {sFile, sData} = oValue; let oData; try { oData = JSON.parse(sData); } catch (e) { - // Silence possible dependency library invalid json errors as they are not critical - // and we should not fail BCP: 1870560058 + // Dependency library files are not critical; log and continue instead of failing. BCP: 1870560058 + if (sData.trim() === "") { + log.verbose(`The file "${sFile}" is empty.`); + } else { + log.verbose(`Failed to parse "${sFile}" as JSON: ${e.message}`); + } } // OpenUI5 build specific - own library can be listed as dependency library also. @@ -1305,12 +1313,16 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, fs.readFile(sPath, 'utf8', (oError, oFileData) => { if (!oError) { - oFileData = JSON.parse(oFileData); - if (oFileData.explored && oFileData.explored.entities && oFileData.explored.entities.length > 0) { - oChainObject.entitiesWithSamples = []; - oFileData.explored.entities.forEach((oEntity) => { - oChainObject.entitiesWithSamples.push(oEntity.id); - }); + try { + oFileData = JSON.parse(oFileData); + if (oFileData.explored && oFileData.explored.entities && oFileData.explored.entities.length > 0) { + oChainObject.entitiesWithSamples = []; + oFileData.explored.entities.forEach((oEntity) => { + oChainObject.entitiesWithSamples.push(oEntity.id); + }); + } + } catch (oParseError) { + log.error(`Failed to parse file "${sPath}": ${oParseError.message}`); } } // We always resolve as this data is not mandatory @@ -1330,9 +1342,17 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, fs.readFile(sInputFile, 'utf8', (oError, sFileData) => { if (oError) { oReject(oError); - } else { + return; + } + try { oChainObject.fileData = JSON.parse(sFileData); oResolve(oChainObject); + } catch (oParseError) { + if (sFileData.trim() === "") { + oReject(new SyntaxError(`The file "${sInputFile}" is empty.`)); + } else { + oReject(new SyntaxError(`Failed to parse "${sInputFile}" as JSON: ${oParseError.message}`)); + } } }); }); diff --git a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js index d9447829451..e1054e84846 100644 --- a/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js +++ b/packages/builder/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -120,6 +120,9 @@ function loadExternalSymbols(apiJsonFolder) { const file = path.join(templateConf.apiJsonFolder, localFileName); try { const sJSON = fs.readFileSync(file, 'UTF-8'); + if (sJSON.trim() === "") { + throw new SyntaxError(`The file is empty.`); + } const data = JSON.parse(sJSON); if ( !Array.isArray(data.symbols) ) { throw new TypeError("api.json does not contain a 'symbols' array");