diff --git a/utils/doclint/api_parser.js b/utils/doclint/api_parser.js index 85f652029a..2bf65784a7 100644 --- a/utils/doclint/api_parser.js +++ b/utils/doclint/api_parser.js @@ -22,6 +22,9 @@ const md = require('../markdown'); const Documentation = require('./documentation'); /** @typedef {import('../markdown').MarkdownNode} MarkdownNode */ +/** @typedef {import('../markdown').MarkdownHeaderNode} MarkdownHeaderNode */ +/** @typedef {import('../markdown').MarkdownLiNode} MarkdownLiNode */ +/** @typedef {import('../markdown').MarkdownTextNode} MarkdownTextNode */ class ApiParser { /** @@ -39,7 +42,7 @@ class ApiParser { bodyParts.push(fs.readFileSync(path.join(apiDir, name)).toString()); } const body = md.parse(bodyParts.join('\n')); - const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : null; + const params = paramsPath ? md.parse(fs.readFileSync(paramsPath).toString()) : undefined; checkNoDuplicateParamEntries(params); const api = params ? applyTemplates(body, params) : body; /** @type {Map} */ @@ -61,7 +64,7 @@ class ApiParser { } /** - * @param {MarkdownNode} node + * @param {MarkdownHeaderNode} node */ parseClass(node) { let extendsName = null; @@ -80,7 +83,7 @@ class ApiParser { /** - * @param {MarkdownNode} spec + * @param {MarkdownHeaderNode} spec */ parseMember(spec) { const match = spec.text.match(/(event|method|property|async method|optional method|optional async method): ([^.]+)\.(.*)/); @@ -112,10 +115,13 @@ class ApiParser { if (match[1].includes('optional')) member.required = false; } - const clazz = this.classes.get(match[2]); + if (!member) + throw new Error('Unknown member: ' + spec.text); + + const clazz = /** @type {Documentation.Class} */(this.classes.get(match[2])); const existingMember = clazz.membersArray.find(m => m.name === name && m.kind === member.kind); if (existingMember && isTypeOverride(existingMember, member)) { - for (const lang of member.langs.only) { + for (const lang of member?.langs?.only || []) { existingMember.langs.types = existingMember.langs.types || {}; existingMember.langs.types[lang] = returnType; } @@ -125,7 +131,7 @@ class ApiParser { } /** - * @param {MarkdownNode} spec + * @param {MarkdownHeaderNode} spec */ parseArgument(spec) { const match = spec.text.match(/(param|option): (.*)/); @@ -173,34 +179,35 @@ class ApiParser { } const p = this.parseProperty(spec); p.required = false; + // @ts-ignore options.type.properties.push(p); } } /** - * @param {MarkdownNode} spec + * @param {MarkdownHeaderNode} spec */ parseProperty(spec) { const param = childrenWithoutProperties(spec)[0]; - const text = param.text; + const text = /** @type {string}*/(param.text); let typeStart = text.indexOf('<'); while ('?e'.includes(text[typeStart - 1])) typeStart--; const name = text.substring(0, typeStart).replace(/\`/g, '').trim(); const comments = extractComments(spec); - const { type, optional } = this.parseType(param); + const { type, optional } = this.parseType(/** @type {MarkdownLiNode} */(param)); return Documentation.Member.createProperty(extractMetainfo(spec), name, type, comments, !optional); } /** - * @param {MarkdownNode=} spec + * @param {MarkdownLiNode} spec * @return {{ type: Documentation.Type, optional: boolean, experimental: boolean }} */ parseType(spec) { const arg = parseVariable(spec.text); const properties = []; - for (const child of spec.children || []) { - const { name, text } = parseVariable(child.text); + for (const child of /** @type {MarkdownLiNode[]} */ (spec.children) || []) { + const { name, text } = parseVariable(/** @type {string} */(child.text)); const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]); const childType = this.parseType(child); properties.push(Documentation.Member.createProperty({ langs: {}, experimental: childType.experimental, since: 'v1.0' }, name, childType.type, comments, !childType.optional)); @@ -271,7 +278,7 @@ function applyTemplates(body, params) { if (!template) throw new Error('Bad template: ' + prop.text); const children = childrenWithoutProperties(template); - const { name: argName } = parseVariable(children[0].text); + const { name: argName } = parseVariable(children[0].text || ''); newChildren.push({ type: node.type, text: name + argName, @@ -301,7 +308,7 @@ function applyTemplates(body, params) { } /** - * @param {MarkdownNode} item + * @param {MarkdownHeaderNode} item * @returns {MarkdownNode[]} */ function extractComments(item) { @@ -323,7 +330,7 @@ function parseApi(apiDir, paramsPath) { } /** - * @param {MarkdownNode} spec + * @param {MarkdownHeaderNode} spec * @returns {import('./documentation').Metainfo} */ function extractMetainfo(spec) { @@ -339,7 +346,7 @@ function extractMetainfo(spec) { * @returns {import('./documentation').Langs} */ function extractLangs(spec) { - for (const child of spec.children) { + for (const child of spec.children || []) { if (child.type !== 'li' || child.liType !== 'bullet' || !child.text.startsWith('langs:')) continue; @@ -347,7 +354,7 @@ function extractLangs(spec) { /** @type {Object} */ const aliases = {}; for (const p of child.children || []) { - const match = p.text.match(/alias-(\w+)[\s]*:(.*)/); + const match = /** @type {string}*/(p.text).match(/alias-(\w+)[\s]*:(.*)/); if (match) aliases[match[1].trim()] = match[2].trim(); } @@ -362,7 +369,7 @@ function extractLangs(spec) { } /** - * @param {MarkdownNode} spec + * @param {MarkdownHeaderNode} spec * @returns {string} */ function extractSince(spec) { @@ -377,7 +384,7 @@ function extractSince(spec) { } /** - * @param {MarkdownNode} spec + * @param {MarkdownHeaderNode} spec * @returns {boolean} */ function extractExperimental(spec) { @@ -389,12 +396,12 @@ function extractSince(spec) { } /** - * @param {MarkdownNode} spec + * @param {MarkdownHeaderNode} spec * @returns {MarkdownNode[]} */ function childrenWithoutProperties(spec) { return (spec.children || []).filter(c => { - const isProperty = c.liType === 'bullet' && (c.text.startsWith('langs:') || c.text.startsWith('since:') || c.text === 'experimental'); + const isProperty = c.type === 'li' && c.liType === 'bullet' && (c.text.startsWith('langs:') || c.text.startsWith('since:') || c.text === 'experimental'); return !isProperty; }); } @@ -405,11 +412,12 @@ function childrenWithoutProperties(spec) { * @returns {boolean} */ function isTypeOverride(existingMember, member) { - if (!existingMember.langs.only) + if (!existingMember.langs.only || !member.langs.only) return true; - if (member.langs.only.every(l => existingMember.langs.only.includes(l))) { + const existingOnly = existingMember.langs.only; + if (member.langs.only.every(l => existingOnly.includes(l))) { return true; - } else if (member.langs.only.some(l => existingMember.langs.only.includes(l))) { + } else if (member.langs.only.some(l => existingOnly.includes(l))) { throw new Error(`Ambiguous language override for: ${member.name}`); } return false; diff --git a/utils/doclint/documentation.js b/utils/doclint/documentation.js index 49bf913ac8..fb25550661 100644 --- a/utils/doclint/documentation.js +++ b/utils/doclint/documentation.js @@ -149,19 +149,19 @@ class Documentation { } /** * @param {Documentation.Class|Documentation.Member|null} classOrMember - * @param {MarkdownNode[]} nodes + * @param {MarkdownNode[] | undefined} nodes */ this._patchLinks = (classOrMember, nodes) => patchLinks(classOrMember, nodes, classesMap, membersMap, linkRenderer); for (const clazz of this.classesArray) - clazz.visit(item => this._patchLinks(item, item.spec)); + clazz.visit(item => this._patchLinks?.(item, item.spec)); } /** * @param {MarkdownNode[]} nodes */ renderLinksInText(nodes) { - this._patchLinks(null, nodes); + this._patchLinks?.(null, nodes); } generateSourceCodeComments() { @@ -192,11 +192,8 @@ Documentation.Class = class { this.extends = extendsName; this.comment = ''; this.index(); - const match = name.match(/(API|JS|CDP|[A-Z])(.*)/); + const match = /** @type {string[]} */(name.match(/(API|JS|CDP|[A-Z])(.*)/)); this.varName = match[1].toLowerCase() + match[2]; - } - - index() { /** @type {!Map} */ this.members = new Map(); /** @type {!Map} */ @@ -211,6 +208,16 @@ Documentation.Class = class { this.events = new Map(); /** @type {!Array} */ this.eventsArray = []; + } + + index() { + this.members = new Map(); + this.properties = new Map(); + this.propertiesArray = []; + this.methods = new Map(); + this.methodsArray = []; + this.events = new Map(); + this.eventsArray = []; for (const member of this.membersArray) { this.members.set(member.name, member); @@ -342,7 +349,7 @@ Documentation.Member = class { /** @type {!Map} */ this.args = new Map(); this.index(); - /** @type {!Documentation.Class} */ + /** @type {!Documentation.Class | null} */ this.clazz = null; /** @type {Documentation.Member=} */ this.enclosingMethod = undefined; @@ -357,13 +364,13 @@ Documentation.Member = class { this.alias = name; this.overloadIndex = 0; if (name.includes('#')) { - const match = name.match(/(.*)#(.*)/); + const match = /** @type {string[]} */(name.match(/(.*)#(.*)/)); this.alias = match[1]; this.overloadIndex = (+match[2]) - 1; } /** * Param is true and option false - * @type {Boolean} + * @type {Boolean | null} */ this.paramOrOption = null; } @@ -376,7 +383,9 @@ Documentation.Member = class { this.args.set(arg.name, arg); arg.enclosingMethod = this; if (arg.name === 'options') { + // @ts-ignore arg.type.properties.sort((p1, p2) => p1.name.localeCompare(p2.name)); + // @ts-ignore arg.type.properties.forEach(p => p.enclosingMethod = this); } } @@ -386,6 +395,8 @@ Documentation.Member = class { * @param {string} lang */ filterForLanguage(lang) { + if (!this.type) + return; if (this.langs.aliases && this.langs.aliases[lang]) this.alias = this.langs.aliases[lang]; if (this.langs.types && this.langs.types[lang]) @@ -397,8 +408,10 @@ Documentation.Member = class { continue; const overriddenArg = (arg.langs.overrides && arg.langs.overrides[lang]) || arg; overriddenArg.filterForLanguage(lang); + // @ts-ignore if (overriddenArg.name === 'options' && !overriddenArg.type.properties.length) continue; + // @ts-ignore overriddenArg.type.filterForLanguage(lang); argsArray.push(overriddenArg); } @@ -406,10 +419,12 @@ Documentation.Member = class { } filterOutExperimental() { + if (!this.type) + return; this.type.filterOutExperimental(); const argsArray = []; for (const arg of this.argsArray) { - if (arg.experimental) + if (arg.experimental || !arg.type) continue; arg.type.filterOutExperimental(); argsArray.push(arg); @@ -418,7 +433,7 @@ Documentation.Member = class { } clone() { - const result = new Documentation.Member(this.kind, { langs: this.langs, experimental: this.experimental, since: this.since }, this.name, this.type.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required); + const result = new Documentation.Member(this.kind, { langs: this.langs, experimental: this.experimental, since: this.since }, this.name, this.type?.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required); result.alias = this.alias; result.async = this.async; result.paramOrOption = this.paramOrOption; @@ -512,6 +527,7 @@ Documentation.Type = class { if (!inUnion && (parsedType.union || parsedType.unionName)) { const type = new Documentation.Type(parsedType.unionName || ''); type.union = []; + // @ts-ignore for (let t = parsedType; t; t = t.union) { const nestedUnion = !!t.unionName && t !== parsedType; type.union.push(Documentation.Type.fromParsedType(t, !nestedUnion)); @@ -524,15 +540,17 @@ Documentation.Type = class { if (parsedType.args) { const type = new Documentation.Type('function'); type.args = []; + // @ts-ignore for (let t = parsedType.args; t; t = t.next) type.args.push(Documentation.Type.fromParsedType(t)); - type.returnType = parsedType.retType ? Documentation.Type.fromParsedType(parsedType.retType) : null; + type.returnType = parsedType.retType ? Documentation.Type.fromParsedType(parsedType.retType) : undefined; return type; } if (parsedType.template) { const type = new Documentation.Type(parsedType.name); type.templates = []; + // @ts-ignore for (let t = parsedType.template; t; t = t.next) type.templates.push(Documentation.Type.fromParsedType(t)); return type; @@ -597,7 +615,7 @@ Documentation.Type = class { } /** - * @returns {Documentation.Member[]} + * @returns {Documentation.Member[] | undefined} */ sortedProperties() { if (!this.properties) @@ -653,7 +671,7 @@ Documentation.Type = class { }; /** - * @param {ParsedType} type + * @param {ParsedType | null} type * @returns {boolean} */ function isStringUnion(type) { @@ -744,7 +762,7 @@ function matchingBracket(str, open, close) { /** * @param {Documentation.Class|Documentation.Member|null} classOrMember - * @param {MarkdownNode[]} spec + * @param {MarkdownNode[]|undefined} spec * @param {Map} classesMap * @param {Map} membersMap * @param {Renderer} linkRenderer @@ -790,16 +808,17 @@ function patchLinks(classOrMember, spec, classesMap, membersMap, linkRenderer) { } /** - * @param {MarkdownNode[]} spec + * @param {MarkdownNode[] | undefined} spec */ function generateSourceCodeComment(spec) { const comments = (spec || []).filter(n => !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c)); md.visitAll(comments, node => { if (node.codeLang && node.codeLang.includes('tab=js-js')) node.type = 'null'; - if (node.liType === 'bullet') + if (node.type === 'li' && node.liType === 'bullet') node.liType = 'default'; if (node.type === 'note') { + // @ts-ignore node.type = 'text'; node.text = '> NOTE: ' + node.text; } diff --git a/utils/markdown.js b/utils/markdown.js index b3d180e77b..a98e9ce90d 100644 --- a/utils/markdown.js +++ b/utils/markdown.js @@ -17,14 +17,52 @@ // @ts-check /** @typedef {{ - * type: 'text' | 'li' | 'code' | 'properties' | 'h0' | 'h1' | 'h2' | 'h3' | 'h4' | 'note' | 'null', + * type: string, * text?: string, + * children?: MarkdownNode[], * codeLang?: string, - * noteType?: string, - * lines?: string[], - * liType?: 'default' | 'bullet' | 'ordinal', - * children?: MarkdownNode[] - * }} MarkdownNode */ + * }} MarkdownBaseNode */ + +/** @typedef {MarkdownBaseNode & { + * type: 'text', + * text: string, + * }} MarkdownTextNode */ + +/** @typedef {MarkdownBaseNode & { + * type: 'h0' | 'h1' | 'h2' | 'h3' | 'h4', + * text: string, + * children: MarkdownNode[] + * }} MarkdownHeaderNode */ + +/** @typedef {MarkdownBaseNode & { + * type: 'li', + * text: string, + * liType: 'default' | 'bullet' | 'ordinal', + * children: MarkdownNode[] + * }} MarkdownLiNode */ + +/** @typedef {MarkdownBaseNode & { + * type: 'code', + * lines: string[], + * codeLang: string, + * }} MarkdownCodeNode */ + +/** @typedef {MarkdownBaseNode & { + * type: 'note', + * text: string, + * noteType: string, + * }} MarkdownNoteNode */ + +/** @typedef {MarkdownBaseNode & { + * type: 'null', + * }} MarkdownNullNode */ + +/** @typedef {MarkdownBaseNode & { + * type: 'properties', + * lines: string[], + * }} MarkdownPropsNode */ + +/** @typedef {MarkdownTextNode | MarkdownLiNode | MarkdownCodeNode | MarkdownNoteNode | MarkdownHeaderNode | MarkdownNullNode | MarkdownPropsNode } MarkdownNode */ function flattenWrappedLines(content) { const inLines = content.replace(/\r\n/g, '\n').split('\n'); @@ -110,13 +148,13 @@ function buildTree(lines) { else break; } - headerStack[0].children.push(node); + /** @type {MarkdownNode[]}*/(headerStack[0].children).push(node); headerStack.unshift(node); continue; } // Remaining items respect indent-based nesting. - const [, indent, content] = line.match('^([ ]*)(.*)'); + const [, indent, content] = /** @type {string[]} */ (line.match('^([ ]*)(.*)')); if (content.startsWith('```')) { /** @type {MarkdownNode} */ const node = { @@ -143,10 +181,10 @@ function buildTree(lines) { if (content.startsWith(':::')) { /** @type {MarkdownNode} */ - const node = { + const node = /** @type {MarkdownNoteNode} */ ({ type: 'note', noteType: content.substring(3) - }; + }); line = lines[++i]; const tokens = []; while (!line.trim().startsWith(':::')) { @@ -184,16 +222,17 @@ function buildTree(lines) { const liType = content.match(/^(-|1.|\*) /); const node = /** @type {MarkdownNode} */({ type: 'text', text: content }); if (liType) { - node.type = 'li'; - node.text = content.substring(liType[0].length); + const liNode = /** @type {MarkdownLiNode} */(node); + liNode.type = 'li'; + liNode.text = content.substring(liType[0].length); if (content.startsWith('1.')) - node.liType = 'ordinal'; + liNode.liType = 'ordinal'; else if (content.startsWith('*')) - node.liType = 'bullet'; + liNode.liType = 'bullet'; else - node.liType = 'default'; + liNode.liType = 'default'; } - const match = node.text.match(/\*\*langs: (.*)\*\*(.*)/); + const match = node.text?.match(/\*\*langs: (.*)\*\*(.*)/); if (match) { node.codeLang = match[1]; node.text = match[2]; @@ -220,7 +259,7 @@ function render(nodes, maxColumns) { for (let node of nodes) { if (node.type === 'null') continue; - innerRenderMdNode('', node, lastNode, result, maxColumns); + innerRenderMdNode('', node, /** @type {MarkdownNode} */ (lastNode), result, maxColumns); lastNode = node; } return result.join('\n'); @@ -240,9 +279,10 @@ function innerRenderMdNode(indent, node, lastNode, result, maxColumns) { }; if (node.type.startsWith('h')) { + const headerNode = /** @type {MarkdownHeaderNode} */ (node); newLine(); const depth = +node.type.substring(1); - result.push(`${'#'.repeat(depth)} ${node.text}`); + result.push(`${'#'.repeat(depth)} ${headerNode.text}`); let lastNode = node; for (const child of node.children || []) { innerRenderMdNode('', child, lastNode, result, maxColumns);