mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-11 12:33:45 +03:00
184 lines
5.9 KiB
JavaScript
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;
|
|
}
|