playwright/utils/doclint/missingDocs.js

184 lines
5.9 KiB
JavaScript

/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* 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 ts = require('typescript');
const EventEmitter = require('events');
const Documentation = require('./documentation');
const path = require('path');
/** @typedef {import('../../markdown').MarkdownNode} MarkdownNode */
const IGNORE_CLASSES = ['PlaywrightAssertions', 'LocatorAssertions', 'PageAssertions', 'APIResponseAssertions', 'ScreenshotAssertions'];
module.exports = function lint(documentation, jsSources, apiFileName) {
const errors = [];
documentation.copyDocsFromSuperclasses(errors);
const apiMethods = listMethods(jsSources, apiFileName);
for (const [className, methods] of apiMethods) {
const docClass = documentation.classes.get(className);
if (!docClass) {
errors.push(`Missing documentation for "${className}"`);
continue;
}
for (const [methodName, params] of methods) {
const member = docClass.membersArray.find(m => m.alias === methodName && m.kind !== 'event');
if (!member) {
errors.push(`Missing documentation for "${className}.${methodName}"`);
continue;
}
const memberParams = paramsForMember(member);
for (const paramName of params) {
if (!memberParams.has(paramName))
errors.push(`Missing documentation for "${className}.${methodName}.${paramName}"`);
}
}
}
for (const cls of documentation.classesArray) {
if (IGNORE_CLASSES.includes(cls.name))
continue;
const methods = apiMethods.get(cls.name);
if (!methods) {
errors.push(`Documented "${cls.name}" not found in sources`);
continue;
}
for (const member of cls.membersArray) {
if (member.kind === 'event')
continue;
const params = methods.get(member.alias);
if (!params) {
errors.push(`Documented "${cls.name}.${member.alias}" not found in sources`);
continue;
}
const memberParams = paramsForMember(member);
for (const paramName of memberParams) {
if (!params.has(paramName) && paramName !== 'options')
errors.push(`Documented "${cls.name}.${member.alias}.${paramName}" not found in sources`);
}
}
}
return errors;
};
/**
* @param {!Documentation.Member} member
*/
function paramsForMember(member) {
if (member.kind !== 'method')
return new Set();
return new Set(member.argsArray.map(a => a.alias));
}
/**
* @param {string[]} rootNames
*/
function listMethods(rootNames, apiFileName) {
const program = ts.createProgram({
options: {
allowJs: true,
target: ts.ScriptTarget.ESNext,
strict: true
},
rootNames
});
const checker = program.getTypeChecker();
const apiClassNames = new Set();
const apiMethods = new Map();
const apiSource = program.getSourceFiles().find(f => f.fileName === apiFileName.split(path.sep).join(path.posix.sep));
/**
* @param {ts.Type} type
*/
function signatureForType(type) {
const signatures = type.getCallSignatures();
if (signatures.length)
return signatures[signatures.length - 1];
if (type.isUnion()) {
const innerTypes = type.types.filter(t => !(t.flags & ts.TypeFlags.Undefined));
if (innerTypes.length === 1)
return signatureForType(innerTypes[0]);
}
return null;
}
/**
* @param {string} className
* @param {string} methodName
*/
function shouldSkipMethodByName(className, methodName) {
if (methodName.startsWith('_') || methodName === 'T' || methodName === 'toString')
return true;
if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(methodName))
return true;
return false;
}
/**
* @param {string} className
* @param {!ts.Type} classType
*/
function visitClass(className, classType) {
let methods = apiMethods.get(className);
if (!methods) {
methods = new Map();
apiMethods.set(className, methods);
}
for (const [name, member] of /** @type {any[]} */(classType.symbol.members || [])) {
if (shouldSkipMethodByName(className, name))
continue;
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
const signature = signatureForType(memberType);
if (signature)
methods.set(name, new Set(signature.parameters.map(p => p.escapedName)));
else
methods.set(name, new Set());
}
for (const baseType of classType.getBaseTypes() || []) {
const baseTypeName = baseType.symbol ? baseType.symbol.name : '';
if (apiClassNames.has(baseTypeName))
visitClass(className, baseType);
}
}
/**
* @param {!ts.Node} node
*/
function visitMethods(node) {
if (ts.isExportSpecifier(node)) {
const className = node.name.text;
const exportSymbol = node.name ? checker.getSymbolAtLocation(node.name) : /** @type {any} */ (node).symbol;
const classType = checker.getDeclaredTypeOfSymbol(exportSymbol);
if (!classType)
throw new Error(`Cannot parse class "${className}"`);
visitClass(className, classType);
}
ts.forEachChild(node, visitMethods);
}
/**
* @param {!ts.Node} node
*/
function visitNames(node) {
if (ts.isExportSpecifier(node))
apiClassNames.add(node.name.text);
ts.forEachChild(node, visitNames);
}
visitNames(apiSource);
visitMethods(apiSource);
return apiMethods;
}