summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/utils/doclint/check_public_api
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /remote/test/puppeteer/utils/doclint/check_public_api
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/test/puppeteer/utils/doclint/check_public_api')
-rw-r--r--remote/test/puppeteer/utils/doclint/check_public_api/Documentation.js157
-rw-r--r--remote/test/puppeteer/utils/doclint/check_public_api/JSBuilder.js279
-rw-r--r--remote/test/puppeteer/utils/doclint/check_public_api/MDBuilder.js402
-rw-r--r--remote/test/puppeteer/utils/doclint/check_public_api/index.js977
4 files changed, 1815 insertions, 0 deletions
diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/Documentation.js b/remote/test/puppeteer/utils/doclint/check_public_api/Documentation.js
new file mode 100644
index 0000000000..9bbaabfc70
--- /dev/null
+++ b/remote/test/puppeteer/utils/doclint/check_public_api/Documentation.js
@@ -0,0 +1,157 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class Documentation {
+ /**
+ * @param {!Array<!Documentation.Class>} classesArray
+ */
+ constructor(classesArray) {
+ this.classesArray = classesArray;
+ /** @type {!Map<string, !Documentation.Class>} */
+ this.classes = new Map();
+ for (const cls of classesArray) this.classes.set(cls.name, cls);
+ }
+}
+
+Documentation.Class = class {
+ /**
+ * @param {string} name
+ * @param {!Array<!Documentation.Member>} membersArray
+ * @param {?string=} extendsName
+ * @param {string=} comment
+ */
+ constructor(name, membersArray, extendsName = null, comment = '') {
+ this.name = name;
+ this.membersArray = membersArray;
+ /** @type {!Map<string, !Documentation.Member>} */
+ this.members = new Map();
+ /** @type {!Map<string, !Documentation.Member>} */
+ this.properties = new Map();
+ /** @type {!Array<!Documentation.Member>} */
+ this.propertiesArray = [];
+ /** @type {!Map<string, !Documentation.Member>} */
+ this.methods = new Map();
+ /** @type {!Array<!Documentation.Member>} */
+ this.methodsArray = [];
+ /** @type {!Map<string, !Documentation.Member>} */
+ this.events = new Map();
+ /** @type {!Array<!Documentation.Member>} */
+ this.eventsArray = [];
+ this.comment = comment;
+ this.extends = extendsName;
+ for (const member of membersArray) {
+ this.members.set(member.name, member);
+ if (member.kind === 'method') {
+ this.methods.set(member.name, member);
+ this.methodsArray.push(member);
+ } else if (member.kind === 'property') {
+ this.properties.set(member.name, member);
+ this.propertiesArray.push(member);
+ } else if (member.kind === 'event') {
+ this.events.set(member.name, member);
+ this.eventsArray.push(member);
+ }
+ }
+ }
+};
+
+Documentation.Member = class {
+ /**
+ * @param {string} kind
+ * @param {string} name
+ * @param {?Documentation.Type} type
+ * @param {!Array<!Documentation.Member>} argsArray
+ */
+ constructor(
+ kind,
+ name,
+ type,
+ argsArray,
+ comment = '',
+ returnComment = '',
+ required = true
+ ) {
+ this.kind = kind;
+ this.name = name;
+ this.type = type;
+ this.comment = comment;
+ this.returnComment = returnComment;
+ this.argsArray = argsArray;
+ this.required = required;
+ /** @type {!Map<string, !Documentation.Member>} */
+ this.args = new Map();
+ for (const arg of argsArray) this.args.set(arg.name, arg);
+ }
+
+ /**
+ * @param {string} name
+ * @param {!Array<!Documentation.Member>} argsArray
+ * @param {?Documentation.Type} returnType
+ * @returns {!Documentation.Member}
+ */
+ static createMethod(name, argsArray, returnType, returnComment, comment) {
+ return new Documentation.Member(
+ 'method',
+ name,
+ returnType,
+ argsArray,
+ comment,
+ returnComment
+ );
+ }
+
+ /**
+ * @param {string} name
+ * @param {!Documentation.Type} type
+ * @param {string=} comment
+ * @param {boolean=} required
+ * @returns {!Documentation.Member}
+ */
+ static createProperty(name, type, comment, required) {
+ return new Documentation.Member(
+ 'property',
+ name,
+ type,
+ [],
+ comment,
+ undefined,
+ required
+ );
+ }
+
+ /**
+ * @param {string} name
+ * @param {?Documentation.Type=} type
+ * @param {string=} comment
+ * @returns {!Documentation.Member}
+ */
+ static createEvent(name, type = null, comment) {
+ return new Documentation.Member('event', name, type, [], comment);
+ }
+};
+
+Documentation.Type = class {
+ /**
+ * @param {string} name
+ * @param {!Array<!Documentation.Member>=} properties
+ */
+ constructor(name, properties = []) {
+ this.name = name;
+ this.properties = properties;
+ }
+};
+
+module.exports = Documentation;
diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/JSBuilder.js b/remote/test/puppeteer/utils/doclint/check_public_api/JSBuilder.js
new file mode 100644
index 0000000000..0994dffcff
--- /dev/null
+++ b/remote/test/puppeteer/utils/doclint/check_public_api/JSBuilder.js
@@ -0,0 +1,279 @@
+const ts = require('typescript');
+const path = require('path');
+const Documentation = require('./Documentation');
+module.exports = checkSources;
+
+/**
+ * @param {!Array<!import('../Source')>} sources
+ */
+function checkSources(sources) {
+ // special treatment for Events.js
+ const classEvents = new Map();
+ const eventsSource = sources.find((source) => source.name() === 'Events.js');
+ if (eventsSource) {
+ const { Events } = require(eventsSource.filePath());
+ for (const [className, events] of Object.entries(Events))
+ classEvents.set(
+ className,
+ Array.from(Object.values(events))
+ .filter((e) => typeof e === 'string')
+ .map((e) => Documentation.Member.createEvent(e))
+ );
+ }
+
+ const excludeClasses = new Set([]);
+ const program = ts.createProgram({
+ options: {
+ allowJs: true,
+ target: ts.ScriptTarget.ES2017,
+ },
+ rootNames: sources.map((source) => source.filePath()),
+ });
+ const checker = program.getTypeChecker();
+ const sourceFiles = program.getSourceFiles();
+ /** @type {!Array<!Documentation.Class>} */
+ const classes = [];
+ /** @type {!Map<string, string>} */
+ const inheritance = new Map();
+
+ const sourceFilesNoNodeModules = sourceFiles.filter(
+ (x) => !x.fileName.includes('node_modules')
+ );
+ const sourceFileNamesSet = new Set(
+ sourceFilesNoNodeModules.map((x) => x.fileName)
+ );
+ sourceFilesNoNodeModules.map((x) => {
+ if (x.fileName.includes('/lib/')) {
+ const potentialTSSource = x.fileName
+ .replace('lib', 'src')
+ .replace('.js', '.ts');
+ if (sourceFileNamesSet.has(potentialTSSource)) {
+ /* Not going to visit this file because we have the TypeScript src code
+ * which we'll use instead.
+ */
+ return;
+ }
+ }
+
+ visit(x);
+ });
+
+ const errors = [];
+ const documentation = new Documentation(
+ recreateClassesWithInheritance(classes, inheritance)
+ );
+
+ return { errors, documentation };
+
+ /**
+ * @param {!Array<!Documentation.Class>} classes
+ * @param {!Map<string, string>} inheritance
+ * @returns {!Array<!Documentation.Class>}
+ */
+ function recreateClassesWithInheritance(classes, inheritance) {
+ const classesByName = new Map(classes.map((cls) => [cls.name, cls]));
+ return classes.map((cls) => {
+ const membersMap = new Map();
+ for (let wp = cls; wp; wp = classesByName.get(inheritance.get(wp.name))) {
+ for (const member of wp.membersArray) {
+ // Member was overridden.
+ const memberId = member.kind + ':' + member.name;
+ if (membersMap.has(memberId)) continue;
+ membersMap.set(memberId, member);
+ }
+ }
+ return new Documentation.Class(cls.name, Array.from(membersMap.values()));
+ });
+ }
+
+ /**
+ * @param {!ts.Node} node
+ */
+ function visit(node) {
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
+ const symbol = node.name
+ ? checker.getSymbolAtLocation(node.name)
+ : node.symbol;
+ let className = symbol.getName();
+
+ if (className === '__class') {
+ let parent = node;
+ while (parent.parent) parent = parent.parent;
+ className = path.basename(parent.fileName, '.js');
+ }
+ if (className && !excludeClasses.has(className)) {
+ classes.push(serializeClass(className, symbol, node));
+ const parentClassName = parentClass(node);
+ if (parentClassName) inheritance.set(className, parentClassName);
+ excludeClasses.add(className);
+ }
+ }
+ ts.forEachChild(node, visit);
+ }
+
+ function parentClass(classNode) {
+ for (const herigateClause of classNode.heritageClauses || []) {
+ for (const heritageType of herigateClause.types) {
+ const parentClassName = heritageType.expression.escapedText;
+ return parentClassName;
+ }
+ }
+ return null;
+ }
+
+ function serializeSymbol(symbol, circular = []) {
+ const type = checker.getTypeOfSymbolAtLocation(
+ symbol,
+ symbol.valueDeclaration
+ );
+ const name = symbol.getName();
+ if (symbol.valueDeclaration && symbol.valueDeclaration.dotDotDotToken) {
+ try {
+ const innerType = serializeType(type.typeArguments[0], circular);
+ innerType.name = '...' + innerType.name;
+ return Documentation.Member.createProperty('...' + name, innerType);
+ } catch (error) {
+ /**
+ * DocLint struggles with the paramArgs type on CDPSession.send because
+ * it uses a complex type from the devtools-protocol method. Doclint
+ * isn't going to be here for much longer so we'll just silence this
+ * warning than try to add support which would warrant a huge rewrite.
+ */
+ if (name !== 'paramArgs') throw error;
+ }
+ }
+ return Documentation.Member.createProperty(
+ name,
+ serializeType(type, circular)
+ );
+ }
+
+ /**
+ * @param {!ts.ObjectType} type
+ */
+ function isRegularObject(type) {
+ if (type.isIntersection()) return true;
+ if (!type.objectFlags) return false;
+ if (!('aliasSymbol' in type)) return false;
+ if (type.getConstructSignatures().length) return false;
+ if (type.getCallSignatures().length) return false;
+ if (type.isLiteral()) return false;
+ if (type.isUnion()) return false;
+
+ return true;
+ }
+
+ /**
+ * @param {!ts.Type} type
+ * @returns {!Documentation.Type}
+ */
+ function serializeType(type, circular = []) {
+ let typeName = checker.typeToString(type);
+ if (
+ typeName === 'any' ||
+ typeName === '{ [x: string]: string; }' ||
+ typeName === '{}'
+ )
+ typeName = 'Object';
+ const nextCircular = [typeName].concat(circular);
+
+ if (isRegularObject(type)) {
+ let properties = undefined;
+ if (!circular.includes(typeName))
+ properties = type
+ .getProperties()
+ .map((property) => serializeSymbol(property, nextCircular));
+ return new Documentation.Type('Object', properties);
+ }
+ if (type.isUnion() && typeName.includes('|')) {
+ const types = type.types.map((type) => serializeType(type, circular));
+ const name = types.map((type) => type.name).join('|');
+ const properties = [].concat(...types.map((type) => type.properties));
+ return new Documentation.Type(
+ name.replace(/false\|true/g, 'boolean'),
+ properties
+ );
+ }
+ if (type.typeArguments) {
+ const properties = [];
+ const innerTypeNames = [];
+ for (const typeArgument of type.typeArguments) {
+ const innerType = serializeType(typeArgument, nextCircular);
+ if (innerType.properties) properties.push(...innerType.properties);
+ innerTypeNames.push(innerType.name);
+ }
+ if (
+ innerTypeNames.length === 0 ||
+ (innerTypeNames.length === 1 && innerTypeNames[0] === 'void')
+ )
+ return new Documentation.Type(type.symbol.name);
+ return new Documentation.Type(
+ `${type.symbol.name}<${innerTypeNames.join(', ')}>`,
+ properties
+ );
+ }
+ return new Documentation.Type(typeName, []);
+ }
+
+ /**
+ * @param {!ts.Symbol} symbol
+ * @returns {boolean}
+ */
+ function symbolHasPrivateModifier(symbol) {
+ const modifiers =
+ (symbol.valueDeclaration && symbol.valueDeclaration.modifiers) || [];
+ return modifiers.some(
+ (modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword
+ );
+ }
+
+ /**
+ * @param {string} className
+ * @param {!ts.Symbol} symbol
+ * @returns {}
+ */
+ function serializeClass(className, symbol, node) {
+ /** @type {!Array<!Documentation.Member>} */
+ const members = classEvents.get(className) || [];
+
+ for (const [name, member] of symbol.members || []) {
+ /* Before TypeScript we denoted private methods with an underscore
+ * but in TypeScript we use the private keyword
+ * hence we check for either here.
+ */
+ if (name.startsWith('_') || symbolHasPrivateModifier(member)) continue;
+
+ const memberType = checker.getTypeOfSymbolAtLocation(
+ member,
+ member.valueDeclaration
+ );
+ const signature = memberType.getCallSignatures()[0];
+ if (signature) members.push(serializeSignature(name, signature));
+ else members.push(serializeProperty(name, memberType));
+ }
+
+ return new Documentation.Class(className, members);
+ }
+
+ /**
+ * @param {string} name
+ * @param {!ts.Signature} signature
+ */
+ function serializeSignature(name, signature) {
+ const parameters = signature.parameters.map((s) => serializeSymbol(s));
+ const returnType = serializeType(signature.getReturnType());
+ return Documentation.Member.createMethod(
+ name,
+ parameters,
+ returnType.name !== 'void' ? returnType : null
+ );
+ }
+
+ /**
+ * @param {string} name
+ * @param {!ts.Type} type
+ */
+ function serializeProperty(name, type) {
+ return Documentation.Member.createProperty(name, serializeType(type));
+ }
+}
diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/MDBuilder.js b/remote/test/puppeteer/utils/doclint/check_public_api/MDBuilder.js
new file mode 100644
index 0000000000..46a4e6cfce
--- /dev/null
+++ b/remote/test/puppeteer/utils/doclint/check_public_api/MDBuilder.js
@@ -0,0 +1,402 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const Documentation = require('./Documentation');
+const commonmark = require('commonmark');
+
+class MDOutline {
+ /**
+ * @param {!Page} page
+ * @param {string} text
+ * @returns {!MDOutline}
+ */
+ static async create(page, text) {
+ // Render markdown as HTML.
+ const reader = new commonmark.Parser();
+ const parsed = reader.parse(text);
+ const writer = new commonmark.HtmlRenderer();
+ const html = writer.render(parsed);
+
+ page.on('console', (msg) => {
+ console.log(msg.text());
+ });
+ // Extract headings.
+ await page.setContent(html);
+ const { classes, errors } = await page.evaluate(() => {
+ const classes = [];
+ const errors = [];
+ const headers = document.body.querySelectorAll('h3');
+ for (let i = 0; i < headers.length; i++) {
+ const fragment = extractSiblingsIntoFragment(
+ headers[i],
+ headers[i + 1]
+ );
+ classes.push(parseClass(fragment));
+ }
+ return { classes, errors };
+
+ /**
+ * @param {HTMLLIElement} element
+ */
+ function parseProperty(element) {
+ const clone = element.cloneNode(true);
+ const ul = clone.querySelector(':scope > ul');
+ const str = parseComment(
+ extractSiblingsIntoFragment(clone.firstChild, ul)
+ );
+ const name = str
+ .substring(0, str.indexOf('<'))
+ .replace(/\`/g, '')
+ .trim();
+ const type = findType(str);
+ const properties = [];
+ const comment = str
+ .substring(str.indexOf('<') + type.length + 2)
+ .trim();
+ // Strings have enum values instead of properties
+ if (!type.includes('string')) {
+ for (const childElement of element.querySelectorAll(
+ ':scope > ul > li'
+ )) {
+ const property = parseProperty(childElement);
+ property.required = property.comment.includes('***required***');
+ properties.push(property);
+ }
+ }
+ return {
+ name,
+ type,
+ comment,
+ properties,
+ };
+ }
+
+ /**
+ * @param {string} str
+ * @returns {string}
+ */
+ function findType(str) {
+ const start = str.indexOf('<') + 1;
+ let count = 1;
+ for (let i = start; i < str.length; i++) {
+ if (str[i] === '<') count++;
+ if (str[i] === '>') count--;
+ if (!count) return str.substring(start, i);
+ }
+ return 'unknown';
+ }
+
+ /**
+ * @param {DocumentFragment} content
+ */
+ function parseClass(content) {
+ const members = [];
+ const headers = content.querySelectorAll('h4');
+ const name = content.firstChild.textContent;
+ let extendsName = null;
+ let commentStart = content.firstChild.nextSibling;
+ const extendsElement = content.querySelector('ul');
+ if (
+ extendsElement &&
+ extendsElement.textContent.trim().startsWith('extends:')
+ ) {
+ commentStart = extendsElement.nextSibling;
+ extendsName = extendsElement.querySelector('a').textContent;
+ }
+ const comment = parseComment(
+ extractSiblingsIntoFragment(commentStart, headers[0])
+ );
+ for (let i = 0; i < headers.length; i++) {
+ const fragment = extractSiblingsIntoFragment(
+ headers[i],
+ headers[i + 1]
+ );
+ members.push(parseMember(fragment));
+ }
+ return {
+ name,
+ comment,
+ extendsName,
+ members,
+ };
+ }
+
+ /**
+ * @param {Node} content
+ */
+ function parseComment(content) {
+ for (const code of content.querySelectorAll('pre > code'))
+ code.replaceWith(
+ '```' +
+ code.className.substring('language-'.length) +
+ '\n' +
+ code.textContent +
+ '```'
+ );
+ for (const code of content.querySelectorAll('code'))
+ code.replaceWith('`' + code.textContent + '`');
+ for (const strong of content.querySelectorAll('strong'))
+ strong.replaceWith('**' + parseComment(strong) + '**');
+ return content.textContent.trim();
+ }
+
+ /**
+ * @param {string} name
+ * @param {DocumentFragment} content
+ */
+ function parseMember(content) {
+ const name = content.firstChild.textContent;
+ const args = [];
+ let returnType = null;
+
+ const paramRegex = /^\w+\.[\w$]+\((.*)\)$/;
+ const matches = paramRegex.exec(name) || ['', ''];
+ const parameters = matches[1];
+ const optionalStartIndex = parameters.indexOf('[');
+ const optinalParamsStr =
+ optionalStartIndex !== -1
+ ? parameters.substring(optionalStartIndex).replace(/[\[\]]/g, '')
+ : '';
+ const optionalparams = new Set(
+ optinalParamsStr
+ .split(',')
+ .filter((x) => x)
+ .map((x) => x.trim())
+ );
+ const ul = content.querySelector('ul');
+ for (const element of content.querySelectorAll('h4 + ul > li')) {
+ if (
+ element.matches('li') &&
+ element.textContent.trim().startsWith('<')
+ ) {
+ returnType = parseProperty(element);
+ } else if (
+ element.matches('li') &&
+ element.firstChild.matches &&
+ element.firstChild.matches('code')
+ ) {
+ const property = parseProperty(element);
+ property.required = !optionalparams.has(property.name);
+ args.push(property);
+ } else if (
+ element.matches('li') &&
+ element.firstChild.nodeType === Element.TEXT_NODE &&
+ element.firstChild.textContent.toLowerCase().startsWith('return')
+ ) {
+ returnType = parseProperty(element);
+ const expectedText = 'returns: ';
+ let actualText = element.firstChild.textContent;
+ let angleIndex = actualText.indexOf('<');
+ let spaceIndex = actualText.indexOf(' ');
+ angleIndex = angleIndex === -1 ? actualText.length : angleIndex;
+ spaceIndex = spaceIndex === -1 ? actualText.length : spaceIndex + 1;
+ actualText = actualText.substring(
+ 0,
+ Math.min(angleIndex, spaceIndex)
+ );
+ if (actualText !== expectedText)
+ errors.push(
+ `${name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`
+ );
+ }
+ }
+ const comment = parseComment(
+ extractSiblingsIntoFragment(ul ? ul.nextSibling : content)
+ );
+ return {
+ name,
+ args,
+ returnType,
+ comment,
+ };
+ }
+
+ /**
+ * @param {!Node} fromInclusive
+ * @param {!Node} toExclusive
+ * @returns {!DocumentFragment}
+ */
+ function extractSiblingsIntoFragment(fromInclusive, toExclusive) {
+ const fragment = document.createDocumentFragment();
+ let node = fromInclusive;
+ while (node && node !== toExclusive) {
+ const next = node.nextSibling;
+ fragment.appendChild(node);
+ node = next;
+ }
+ return fragment;
+ }
+ });
+ return new MDOutline(classes, errors);
+ }
+
+ constructor(classes, errors) {
+ this.classes = [];
+ this.errors = errors;
+ const classHeading = /^class: (\w+)$/;
+ const constructorRegex = /^new (\w+)\((.*)\)$/;
+ const methodRegex = /^(\w+)\.([\w$]+)\((.*)\)$/;
+ const propertyRegex = /^(\w+)\.(\w+)$/;
+ const eventRegex = /^event: '(\w+)'$/;
+ let currentClassName = null;
+ let currentClassMembers = [];
+ let currentClassComment = '';
+ let currentClassExtends = null;
+ for (const cls of classes) {
+ const match = cls.name.match(classHeading);
+ if (!match) continue;
+ currentClassName = match[1];
+ currentClassComment = cls.comment;
+ currentClassExtends = cls.extendsName;
+ for (const member of cls.members) {
+ if (constructorRegex.test(member.name)) {
+ const match = member.name.match(constructorRegex);
+ handleMethod.call(this, member, match[1], 'constructor', match[2]);
+ } else if (methodRegex.test(member.name)) {
+ const match = member.name.match(methodRegex);
+ handleMethod.call(this, member, match[1], match[2], match[3]);
+ } else if (propertyRegex.test(member.name)) {
+ const match = member.name.match(propertyRegex);
+ handleProperty.call(this, member, match[1], match[2]);
+ } else if (eventRegex.test(member.name)) {
+ const match = member.name.match(eventRegex);
+ handleEvent.call(this, member, match[1]);
+ }
+ }
+ flushClassIfNeeded.call(this);
+ }
+
+ function handleMethod(member, className, methodName, parameters) {
+ if (
+ !currentClassName ||
+ !className ||
+ !methodName ||
+ className.toLowerCase() !== currentClassName.toLowerCase()
+ ) {
+ this.errors.push(`Failed to process header as method: ${member.name}`);
+ return;
+ }
+ parameters = parameters.trim().replace(/[\[\]]/g, '');
+ if (parameters !== member.args.map((arg) => arg.name).join(', '))
+ this.errors.push(
+ `Heading arguments for "${
+ member.name
+ }" do not match described ones, i.e. "${parameters}" != "${member.args
+ .map((a) => a.name)
+ .join(', ')}"`
+ );
+ const args = member.args.map(createPropertyFromJSON);
+ let returnType = null;
+ let returnComment = '';
+ if (member.returnType) {
+ const returnProperty = createPropertyFromJSON(member.returnType);
+ returnType = returnProperty.type;
+ returnComment = returnProperty.comment;
+ }
+ const method = Documentation.Member.createMethod(
+ methodName,
+ args,
+ returnType,
+ returnComment,
+ member.comment
+ );
+ currentClassMembers.push(method);
+ }
+
+ function createPropertyFromJSON(payload) {
+ const type = new Documentation.Type(
+ payload.type,
+ payload.properties.map(createPropertyFromJSON)
+ );
+ const required = payload.required;
+ return Documentation.Member.createProperty(
+ payload.name,
+ type,
+ payload.comment,
+ required
+ );
+ }
+
+ function handleProperty(member, className, propertyName) {
+ if (
+ !currentClassName ||
+ !className ||
+ !propertyName ||
+ className.toLowerCase() !== currentClassName.toLowerCase()
+ ) {
+ this.errors.push(
+ `Failed to process header as property: ${member.name}`
+ );
+ return;
+ }
+ const type = member.returnType ? member.returnType.type : null;
+ const properties = member.returnType ? member.returnType.properties : [];
+ currentClassMembers.push(
+ createPropertyFromJSON({
+ type,
+ name: propertyName,
+ properties,
+ comment: member.comment,
+ })
+ );
+ }
+
+ function handleEvent(member, eventName) {
+ if (!currentClassName || !eventName) {
+ this.errors.push(`Failed to process header as event: ${member.name}`);
+ return;
+ }
+ currentClassMembers.push(
+ Documentation.Member.createEvent(
+ eventName,
+ member.returnType && createPropertyFromJSON(member.returnType).type,
+ member.comment
+ )
+ );
+ }
+
+ function flushClassIfNeeded() {
+ if (currentClassName === null) return;
+ this.classes.push(
+ new Documentation.Class(
+ currentClassName,
+ currentClassMembers,
+ currentClassExtends,
+ currentClassComment
+ )
+ );
+ currentClassName = null;
+ currentClassMembers = [];
+ }
+ }
+}
+
+/**
+ * @param {!Page} page
+ * @param {!Array<!Source>} sources
+ * @returns {!Promise<{documentation: !Documentation, errors: !Array<string>}>}
+ */
+module.exports = async function (page, sources) {
+ const classes = [];
+ const errors = [];
+ for (const source of sources) {
+ const outline = await MDOutline.create(page, source.text());
+ classes.push(...outline.classes);
+ errors.push(...outline.errors);
+ }
+ const documentation = new Documentation(classes);
+ return { documentation, errors };
+};
diff --git a/remote/test/puppeteer/utils/doclint/check_public_api/index.js b/remote/test/puppeteer/utils/doclint/check_public_api/index.js
new file mode 100644
index 0000000000..84d5ca0f2a
--- /dev/null
+++ b/remote/test/puppeteer/utils/doclint/check_public_api/index.js
@@ -0,0 +1,977 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const jsBuilder = require('./JSBuilder');
+const mdBuilder = require('./MDBuilder');
+const Documentation = require('./Documentation');
+const Message = require('../Message');
+const {
+ MODULES_TO_CHECK_FOR_COVERAGE,
+} = require('../../../test/coverage-utils');
+
+const EXCLUDE_PROPERTIES = new Set([
+ 'Browser.create',
+ 'Headers.fromPayload',
+ 'Page.create',
+ 'JSHandle.toString',
+ 'TimeoutError.name',
+ /* This isn't an actual property, but a TypeScript generic.
+ * DocLint incorrectly parses it as a property.
+ */
+ 'ElementHandle.ElementType',
+]);
+
+/**
+ * @param {!Page} page
+ * @param {!Array<!Source>} mdSources
+ * @returns {!Promise<!Array<!Message>>}
+ */
+module.exports = async function lint(page, mdSources, jsSources) {
+ const mdResult = await mdBuilder(page, mdSources);
+ const jsResult = await jsBuilder(jsSources);
+ const jsDocumentation = filterJSDocumentation(jsResult.documentation);
+ const mdDocumentation = mdResult.documentation;
+
+ const jsErrors = jsResult.errors;
+ jsErrors.push(...checkDuplicates(jsDocumentation));
+
+ const mdErrors = mdResult.errors;
+ mdErrors.push(...compareDocumentations(mdDocumentation, jsDocumentation));
+ mdErrors.push(...checkDuplicates(mdDocumentation));
+ mdErrors.push(...checkSorting(mdDocumentation));
+
+ // Push all errors with proper prefixes
+ const errors = jsErrors.map((error) => '[JavaScript] ' + error);
+ errors.push(...mdErrors.map((error) => '[MarkDown] ' + error));
+ return errors.map((error) => Message.error(error));
+};
+
+/**
+ * @param {!Documentation} doc
+ * @returns {!Array<string>}
+ */
+function checkSorting(doc) {
+ const errors = [];
+ for (const cls of doc.classesArray) {
+ const members = cls.membersArray;
+
+ // Events should go first.
+ let eventIndex = 0;
+ for (
+ ;
+ eventIndex < members.length && members[eventIndex].kind === 'event';
+ ++eventIndex
+ );
+ for (
+ ;
+ eventIndex < members.length && members[eventIndex].kind !== 'event';
+ ++eventIndex
+ );
+ if (eventIndex < members.length)
+ errors.push(
+ `Events should go first. Event '${members[eventIndex].name}' in class ${cls.name} breaks order`
+ );
+
+ // Constructor should be right after events and before all other members.
+ const constructorIndex = members.findIndex(
+ (member) => member.kind === 'method' && member.name === 'constructor'
+ );
+ if (constructorIndex > 0 && members[constructorIndex - 1].kind !== 'event')
+ errors.push(`Constructor of ${cls.name} should go before other methods`);
+
+ // Events should be sorted alphabetically.
+ for (let i = 0; i < members.length - 1; ++i) {
+ const member1 = cls.membersArray[i];
+ const member2 = cls.membersArray[i + 1];
+ if (member1.kind !== 'event' || member2.kind !== 'event') continue;
+ if (member1.name > member2.name)
+ errors.push(
+ `Event '${member1.name}' in class ${cls.name} breaks alphabetic ordering of events`
+ );
+ }
+
+ // All other members should be sorted alphabetically.
+ for (let i = 0; i < members.length - 1; ++i) {
+ const member1 = cls.membersArray[i];
+ const member2 = cls.membersArray[i + 1];
+ if (member1.kind === 'event' || member2.kind === 'event') continue;
+ if (member1.kind === 'method' && member1.name === 'constructor') continue;
+ if (member1.name > member2.name) {
+ let memberName1 = `${cls.name}.${member1.name}`;
+ if (member1.kind === 'method') memberName1 += '()';
+ let memberName2 = `${cls.name}.${member2.name}`;
+ if (member2.kind === 'method') memberName2 += '()';
+ errors.push(
+ `Bad alphabetic ordering of ${cls.name} members: ${memberName1} should go after ${memberName2}`
+ );
+ }
+ }
+ }
+ return errors;
+}
+
+/**
+ * @param {!Documentation} jsDocumentation
+ * @returns {!Documentation}
+ */
+function filterJSDocumentation(jsDocumentation) {
+ const includedClasses = new Set(Object.keys(MODULES_TO_CHECK_FOR_COVERAGE));
+ // Filter private classes and methods.
+ const classes = [];
+ for (const cls of jsDocumentation.classesArray) {
+ if (includedClasses && !includedClasses.has(cls.name)) continue;
+ const members = cls.membersArray.filter(
+ (member) => !EXCLUDE_PROPERTIES.has(`${cls.name}.${member.name}`)
+ );
+ classes.push(new Documentation.Class(cls.name, members));
+ }
+ return new Documentation(classes);
+}
+
+/**
+ * @param {!Documentation} doc
+ * @returns {!Array<string>}
+ */
+function checkDuplicates(doc) {
+ const errors = [];
+ const classes = new Set();
+ // Report duplicates.
+ for (const cls of doc.classesArray) {
+ if (classes.has(cls.name))
+ errors.push(`Duplicate declaration of class ${cls.name}`);
+ classes.add(cls.name);
+ const members = new Set();
+ for (const member of cls.membersArray) {
+ if (members.has(member.kind + ' ' + member.name))
+ errors.push(
+ `Duplicate declaration of ${member.kind} ${cls.name}.${member.name}()`
+ );
+ members.add(member.kind + ' ' + member.name);
+ const args = new Set();
+ for (const arg of member.argsArray) {
+ if (args.has(arg.name))
+ errors.push(
+ `Duplicate declaration of argument ${cls.name}.${member.name} "${arg.name}"`
+ );
+ args.add(arg.name);
+ }
+ }
+ }
+ return errors;
+}
+
+// All the methods from our EventEmitter that we don't document for each subclass.
+const EVENT_LISTENER_METHODS = new Set([
+ 'emit',
+ 'listenerCount',
+ 'off',
+ 'on',
+ 'once',
+ 'removeListener',
+ 'addListener',
+ 'removeAllListeners',
+]);
+
+/* Methods that are defined in code but are not documented */
+const expectedNotFoundMethods = new Map([
+ ['Browser', EVENT_LISTENER_METHODS],
+ ['BrowserContext', EVENT_LISTENER_METHODS],
+ ['CDPSession', EVENT_LISTENER_METHODS],
+ ['Page', EVENT_LISTENER_METHODS],
+ ['WebWorker', EVENT_LISTENER_METHODS],
+]);
+
+/**
+ * @param {!Documentation} actual
+ * @param {!Documentation} expected
+ * @returns {!Array<string>}
+ */
+function compareDocumentations(actual, expected) {
+ const errors = [];
+
+ const actualClasses = Array.from(actual.classes.keys()).sort();
+ const expectedClasses = Array.from(expected.classes.keys()).sort();
+ const classesDiff = diff(actualClasses, expectedClasses);
+
+ /* These have been moved onto PuppeteerNode but we want to document them under
+ * Puppeteer. See https://github.com/puppeteer/puppeteer/pull/6504 for details.
+ */
+ const expectedPuppeteerClassMissingMethods = new Set([
+ 'createBrowserFetcher',
+ 'defaultArgs',
+ 'executablePath',
+ 'launch',
+ ]);
+
+ for (const className of classesDiff.extra)
+ errors.push(`Non-existing class found: ${className}`);
+
+ for (const className of classesDiff.missing) {
+ if (className === 'PuppeteerNode') {
+ continue;
+ }
+ errors.push(`Class not found: ${className}`);
+ }
+
+ for (const className of classesDiff.equal) {
+ const actualClass = actual.classes.get(className);
+ const expectedClass = expected.classes.get(className);
+ const actualMethods = Array.from(actualClass.methods.keys()).sort();
+ const expectedMethods = Array.from(expectedClass.methods.keys()).sort();
+ const methodDiff = diff(actualMethods, expectedMethods);
+
+ for (const methodName of methodDiff.extra) {
+ if (
+ expectedPuppeteerClassMissingMethods.has(methodName) &&
+ actualClass.name === 'Puppeteer'
+ ) {
+ continue;
+ }
+ errors.push(`Non-existing method found: ${className}.${methodName}()`);
+ }
+
+ for (const methodName of methodDiff.missing) {
+ const missingMethodsForClass = expectedNotFoundMethods.get(className);
+ if (missingMethodsForClass && missingMethodsForClass.has(methodName))
+ continue;
+ errors.push(`Method not found: ${className}.${methodName}()`);
+ }
+
+ for (const methodName of methodDiff.equal) {
+ const actualMethod = actualClass.methods.get(methodName);
+ const expectedMethod = expectedClass.methods.get(methodName);
+ if (!actualMethod.type !== !expectedMethod.type) {
+ if (actualMethod.type)
+ errors.push(
+ `Method ${className}.${methodName} has unneeded description of return type`
+ );
+ else
+ errors.push(
+ `Method ${className}.${methodName} is missing return type description`
+ );
+ } else if (actualMethod.hasReturn) {
+ checkType(
+ `Method ${className}.${methodName} has the wrong return type: `,
+ actualMethod.type,
+ expectedMethod.type
+ );
+ }
+ const actualArgs = Array.from(actualMethod.args.keys());
+ const expectedArgs = Array.from(expectedMethod.args.keys());
+ const argsDiff = diff(actualArgs, expectedArgs);
+
+ if (argsDiff.extra.length || argsDiff.missing.length) {
+ /* Doclint cannot handle the parameter type of the CDPSession send method.
+ * so we just ignore it.
+ */
+ const isCdpSessionSend =
+ className === 'CDPSession' && methodName === 'send';
+ if (!isCdpSessionSend) {
+ const text = [
+ `Method ${className}.${methodName}() fails to describe its parameters:`,
+ ];
+ for (const arg of argsDiff.missing)
+ text.push(`- Argument not found: ${arg}`);
+ for (const arg of argsDiff.extra)
+ text.push(`- Non-existing argument found: ${arg}`);
+ errors.push(text.join('\n'));
+ }
+ }
+
+ for (const arg of argsDiff.equal)
+ checkProperty(
+ `Method ${className}.${methodName}()`,
+ actualMethod.args.get(arg),
+ expectedMethod.args.get(arg)
+ );
+ }
+ const actualProperties = Array.from(actualClass.properties.keys()).sort();
+ const expectedProperties = Array.from(
+ expectedClass.properties.keys()
+ ).sort();
+ const propertyDiff = diff(actualProperties, expectedProperties);
+ for (const propertyName of propertyDiff.extra) {
+ if (className === 'Puppeteer' && propertyName === 'product') {
+ continue;
+ }
+ errors.push(`Non-existing property found: ${className}.${propertyName}`);
+ }
+ for (const propertyName of propertyDiff.missing)
+ errors.push(`Property not found: ${className}.${propertyName}`);
+
+ const actualEvents = Array.from(actualClass.events.keys()).sort();
+ const expectedEvents = Array.from(expectedClass.events.keys()).sort();
+ const eventsDiff = diff(actualEvents, expectedEvents);
+ for (const eventName of eventsDiff.extra)
+ errors.push(
+ `Non-existing event found in class ${className}: '${eventName}'`
+ );
+ for (const eventName of eventsDiff.missing)
+ errors.push(`Event not found in class ${className}: '${eventName}'`);
+ }
+
+ /**
+ * @param {string} source
+ * @param {!Documentation.Member} actual
+ * @param {!Documentation.Member} expected
+ */
+ function checkProperty(source, actual, expected) {
+ checkType(source + ' ' + actual.name, actual.type, expected.type);
+ }
+
+ /**
+ * @param {string} source
+ * @param {!string} actualName
+ * @param {!string} expectedName
+ */
+ function namingMisMatchInTypeIsExpected(source, actualName, expectedName) {
+ /* The DocLint tooling doesn't deal well with generics in TypeScript
+ * source files. We could fix this but the longterm plan is to
+ * auto-generate documentation from TS. So instead we document here
+ * the methods that use generics that DocLint trips up on and if it
+ * finds a mismatch that matches one of the cases below it doesn't
+ * error. This still means we're protected from accidental changes, as
+ * if the mismatch doesn't exactly match what's described below
+ * DocLint will fail.
+ */
+ const expectedNamingMismatches = new Map([
+ [
+ 'Method CDPSession.send() method',
+ {
+ actualName: 'string',
+ expectedName: 'T',
+ },
+ ],
+ [
+ 'Method CDPSession.send() params',
+ {
+ actualName: 'Object',
+ expectedName: 'CommandParameters[T]',
+ },
+ ],
+ [
+ 'Method ElementHandle.click() options',
+ {
+ actualName: 'Object',
+ expectedName: 'ClickOptions',
+ },
+ ],
+ [
+ 'Method ElementHandle.press() options',
+ {
+ actualName: 'Object',
+ expectedName: 'PressOptions',
+ },
+ ],
+ [
+ 'Method ElementHandle.press() key',
+ {
+ actualName: 'string',
+ expectedName: 'KeyInput',
+ },
+ ],
+ [
+ 'Method Keyboard.down() key',
+ {
+ actualName: 'string',
+ expectedName: 'KeyInput',
+ },
+ ],
+ [
+ 'Method Keyboard.press() key',
+ {
+ actualName: 'string',
+ expectedName: 'KeyInput',
+ },
+ ],
+ [
+ 'Method Keyboard.up() key',
+ {
+ actualName: 'string',
+ expectedName: 'KeyInput',
+ },
+ ],
+ [
+ 'Method Mouse.down() options',
+ {
+ actualName: 'Object',
+ expectedName: 'MouseOptions',
+ },
+ ],
+ [
+ 'Method Mouse.up() options',
+ {
+ actualName: 'Object',
+ expectedName: 'MouseOptions',
+ },
+ ],
+ [
+ 'Method Mouse.wheel() options',
+ {
+ actualName: 'Object',
+ expectedName: 'MouseWheelOptions',
+ },
+ ],
+ [
+ 'Method Tracing.start() options',
+ {
+ actualName: 'Object',
+ expectedName: 'TracingOptions',
+ },
+ ],
+ [
+ 'Method Frame.waitForSelector() options',
+ {
+ actualName: 'Object',
+ expectedName: 'WaitForSelectorOptions',
+ },
+ ],
+ [
+ 'Method Frame.waitForXPath() options',
+ {
+ actualName: 'Object',
+ expectedName: 'WaitForSelectorOptions',
+ },
+ ],
+ [
+ 'Method HTTPRequest.abort() errorCode',
+ {
+ actualName: 'string',
+ expectedName: 'ErrorCode',
+ },
+ ],
+ [
+ 'Method Frame.goto() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method Frame.waitForNavigation() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method Frame.setContent() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method Puppeteer.defaultArgs() options',
+ {
+ actualName: 'Object',
+ expectedName: 'ChromeArgOptions',
+ },
+ ],
+ [
+ 'Method Page.goBack() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method Page.goForward() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method Page.goto() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method Page.reload() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method Page.setContent() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method Page.waitForNavigation() options.waitUntil',
+ {
+ actualName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array',
+ expectedName:
+ '"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleEvent>',
+ },
+ ],
+ [
+ 'Method BrowserContext.overridePermissions() permissions',
+ {
+ actualName: 'Array<string>',
+ expectedName: 'Array<PermissionType>',
+ },
+ ],
+ [
+ 'Method Puppeteer.createBrowserFetcher() options',
+ {
+ actualName: 'Object',
+ expectedName: 'BrowserFetcherOptions',
+ },
+ ],
+ [
+ 'Method Page.authenticate() credentials',
+ {
+ actualName: 'Object',
+ expectedName: 'Credentials',
+ },
+ ],
+ [
+ 'Method Page.emulateMediaFeatures() features',
+ {
+ actualName: 'Array<Object>',
+ expectedName: 'Array<MediaFeature>',
+ },
+ ],
+ [
+ 'Method Page.emulate() options.viewport',
+ {
+ actualName: 'Object',
+ expectedName: 'Viewport',
+ },
+ ],
+ [
+ 'Method Page.setViewport() options.viewport',
+ {
+ actualName: 'Object',
+ expectedName: 'Viewport',
+ },
+ ],
+ [
+ 'Method Page.setViewport() viewport',
+ {
+ actualName: 'Object',
+ expectedName: 'Viewport',
+ },
+ ],
+ [
+ 'Method Page.connect() options.defaultViewport',
+ {
+ actualName: 'Object',
+ expectedName: 'Viewport',
+ },
+ ],
+ [
+ 'Method Puppeteer.connect() options.defaultViewport',
+ {
+ actualName: 'Object',
+ expectedName: 'Viewport',
+ },
+ ],
+ [
+ 'Method Puppeteer.launch() options.defaultViewport',
+ {
+ actualName: 'Object',
+ expectedName: 'Viewport',
+ },
+ ],
+ [
+ 'Method Page.launch() options.defaultViewport',
+ {
+ actualName: 'Object',
+ expectedName: 'Viewport',
+ },
+ ],
+ [
+ 'Method Page.goBack() options',
+ {
+ actualName: 'Object',
+ expectedName: 'WaitForOptions',
+ },
+ ],
+ [
+ 'Method Page.goForward() options',
+ {
+ actualName: 'Object',
+ expectedName: 'WaitForOptions',
+ },
+ ],
+ [
+ 'Method Page.reload() options',
+ {
+ actualName: 'Object',
+ expectedName: 'WaitForOptions',
+ },
+ ],
+ [
+ 'Method Page.waitForNavigation() options',
+ {
+ actualName: 'Object',
+ expectedName: 'WaitForOptions',
+ },
+ ],
+ [
+ 'Method Page.pdf() options',
+ {
+ actualName: 'Object',
+ expectedName: 'PDFOptions',
+ },
+ ],
+ [
+ 'Method Page.screenshot() options',
+ {
+ actualName: 'Object',
+ expectedName: 'ScreenshotOptions',
+ },
+ ],
+ [
+ 'Method Page.setContent() options',
+ {
+ actualName: 'Object',
+ expectedName: 'WaitForOptions',
+ },
+ ],
+ [
+ 'Method Page.setCookie() ...cookies',
+ {
+ actualName: '...Object',
+ expectedName: '...CookieParam',
+ },
+ ],
+ [
+ 'Method Page.emulateVisionDeficiency() type',
+ {
+ actualName: 'string',
+ expectedName: 'Object',
+ },
+ ],
+ [
+ 'Method Accessibility.snapshot() options',
+ {
+ actualName: 'Object',
+ expectedName: 'SnapshotOptions',
+ },
+ ],
+ [
+ 'Method Browser.waitForTarget() options',
+ {
+ actualName: 'Object',
+ expectedName: 'WaitForTargetOptions',
+ },
+ ],
+ [
+ 'Method EventEmitter.emit() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'EventType',
+ },
+ ],
+ [
+ 'Method EventEmitter.listenerCount() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'EventType',
+ },
+ ],
+ [
+ 'Method EventEmitter.off() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'EventType',
+ },
+ ],
+ [
+ 'Method EventEmitter.on() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'EventType',
+ },
+ ],
+ [
+ 'Method EventEmitter.once() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'EventType',
+ },
+ ],
+ [
+ 'Method EventEmitter.removeListener() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'EventType',
+ },
+ ],
+ [
+ 'Method EventEmitter.addListener() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'EventType',
+ },
+ ],
+ [
+ 'Method EventEmitter.removeAllListeners() event',
+ {
+ actualName: 'string|symbol',
+ expectedName: 'EventType',
+ },
+ ],
+ [
+ 'Method Coverage.startCSSCoverage() options',
+ {
+ actualName: 'Object',
+ expectedName: 'CSSCoverageOptions',
+ },
+ ],
+ [
+ 'Method Coverage.startJSCoverage() options',
+ {
+ actualName: 'Object',
+ expectedName: 'JSCoverageOptions',
+ },
+ ],
+ [
+ 'Method Mouse.click() options.button',
+ {
+ actualName: '"left"|"right"|"middle"',
+ expectedName: 'MouseButton',
+ },
+ ],
+ [
+ 'Method Frame.click() options.button',
+ {
+ actualName: '"left"|"right"|"middle"',
+ expectedName: 'MouseButton',
+ },
+ ],
+ [
+ 'Method Page.click() options.button',
+ {
+ actualName: '"left"|"right"|"middle"',
+ expectedName: 'MouseButton',
+ },
+ ],
+ [
+ 'Method HTTPRequest.continue() overrides',
+ {
+ actualName: 'Object',
+ expectedName: 'ContinueRequestOverrides',
+ },
+ ],
+ [
+ 'Method HTTPRequest.respond() response',
+ {
+ actualName: 'Object',
+ expectedName: 'ResponseForRequest',
+ },
+ ],
+ [
+ 'Method Frame.addScriptTag() options',
+ {
+ actualName: 'Object',
+ expectedName: 'FrameAddScriptTagOptions',
+ },
+ ],
+ [
+ 'Method Frame.addStyleTag() options',
+ {
+ actualName: 'Object',
+ expectedName: 'FrameAddStyleTagOptions',
+ },
+ ],
+ [
+ 'Method Frame.waitForFunction() options',
+ {
+ actualName: 'Object',
+ expectedName: 'FrameWaitForFunctionOptions',
+ },
+ ],
+ [
+ 'Method BrowserContext.overridePermissions() permissions',
+ {
+ actualName: 'Array<string>',
+ expectedName: 'Array<Object>',
+ },
+ ],
+ [
+ 'Method Puppeteer.connect() options',
+ {
+ actualName: 'Object',
+ expectedName: 'ConnectOptions',
+ },
+ ],
+ ]);
+
+ const expectedForSource = expectedNamingMismatches.get(source);
+ if (!expectedForSource) return false;
+
+ const namingMismatchIsExpected =
+ expectedForSource.actualName === actualName &&
+ expectedForSource.expectedName === expectedName;
+
+ return namingMismatchIsExpected;
+ }
+
+ /**
+ * @param {string} source
+ * @param {!Documentation.Type} actual
+ * @param {!Documentation.Type} expected
+ */
+ function checkType(source, actual, expected) {
+ // TODO(@JoelEinbinder): check functions and Serializable
+ if (actual.name.includes('unction') || actual.name.includes('Serializable'))
+ return;
+ // We don't have nullchecks on for TypeScript
+ const actualName = actual.name.replace(/[\? ]/g, '');
+ // TypeScript likes to add some spaces
+ const expectedName = expected.name.replace(/\ /g, '');
+ const namingMismatchIsExpected = namingMisMatchInTypeIsExpected(
+ source,
+ actualName,
+ expectedName
+ );
+ if (expectedName !== actualName && !namingMismatchIsExpected)
+ errors.push(`${source} ${actualName} != ${expectedName}`);
+
+ /* If we got a naming mismatch and it was expected, don't check the properties
+ * as they will likely be considered "wrong" by DocLint too.
+ */
+ if (namingMismatchIsExpected) return;
+
+ /* Some methods cause errors in the property checks for an unknown reason
+ * so we support a list of methods whose parameters are not checked.
+ */
+ const skipPropertyChecksOnMethods = new Set([
+ 'Method Page.deleteCookie() ...cookies',
+ 'Method Page.setCookie() ...cookies',
+ 'Method Puppeteer.connect() options',
+ ]);
+ if (skipPropertyChecksOnMethods.has(source)) return;
+
+ const actualPropertiesMap = new Map(
+ actual.properties.map((property) => [property.name, property.type])
+ );
+ const expectedPropertiesMap = new Map(
+ expected.properties.map((property) => [property.name, property.type])
+ );
+ const propertiesDiff = diff(
+ Array.from(actualPropertiesMap.keys()).sort(),
+ Array.from(expectedPropertiesMap.keys()).sort()
+ );
+ for (const propertyName of propertiesDiff.extra)
+ errors.push(`${source} has unexpected property ${propertyName}`);
+ for (const propertyName of propertiesDiff.missing)
+ errors.push(`${source} is missing property ${propertyName}`);
+ for (const propertyName of propertiesDiff.equal)
+ checkType(
+ source + '.' + propertyName,
+ actualPropertiesMap.get(propertyName),
+ expectedPropertiesMap.get(propertyName)
+ );
+ }
+
+ return errors;
+}
+
+/**
+ * @param {!Array<string>} actual
+ * @param {!Array<string>} expected
+ * @returns {{extra: !Array<string>, missing: !Array<string>, equal: !Array<string>}}
+ */
+function diff(actual, expected) {
+ const N = actual.length;
+ const M = expected.length;
+ if (N === 0 && M === 0) return { extra: [], missing: [], equal: [] };
+ if (N === 0) return { extra: [], missing: expected.slice(), equal: [] };
+ if (M === 0) return { extra: actual.slice(), missing: [], equal: [] };
+ const d = new Array(N);
+ const bt = new Array(N);
+ for (let i = 0; i < N; ++i) {
+ d[i] = new Array(M);
+ bt[i] = new Array(M);
+ for (let j = 0; j < M; ++j) {
+ const top = val(i - 1, j);
+ const left = val(i, j - 1);
+ if (top > left) {
+ d[i][j] = top;
+ bt[i][j] = 'extra';
+ } else {
+ d[i][j] = left;
+ bt[i][j] = 'missing';
+ }
+ const diag = val(i - 1, j - 1);
+ if (actual[i] === expected[j] && d[i][j] < diag + 1) {
+ d[i][j] = diag + 1;
+ bt[i][j] = 'eq';
+ }
+ }
+ }
+ // Backtrack results.
+ let i = N - 1;
+ let j = M - 1;
+ const missing = [];
+ const extra = [];
+ const equal = [];
+ while (i >= 0 && j >= 0) {
+ switch (bt[i][j]) {
+ case 'extra':
+ extra.push(actual[i]);
+ i -= 1;
+ break;
+ case 'missing':
+ missing.push(expected[j]);
+ j -= 1;
+ break;
+ case 'eq':
+ equal.push(actual[i]);
+ i -= 1;
+ j -= 1;
+ break;
+ }
+ }
+ while (i >= 0) extra.push(actual[i--]);
+ while (j >= 0) missing.push(expected[j--]);
+ extra.reverse();
+ missing.reverse();
+ equal.reverse();
+ return { extra, missing, equal };
+
+ function val(i, j) {
+ return i < 0 || j < 0 ? 0 : d[i][j];
+ }
+}