mirror of
https://github.com/microsoft/playwright.git
synced 2024-08-17 08:00:23 +03:00
chore: remove JS types checker, rely on typescript (#4831)
chore: remove JS types checker, rely on typescript We keep checking that all methods are documented, and no extra methods are documented, but rely on typescript for everything else.
This commit is contained in:
parent
a446792c18
commit
94077e0e74
@ -1,5 +1,4 @@
|
||||
test/assets/modernizr.js
|
||||
utils/doclint/check_public_api/test/
|
||||
lib/
|
||||
*.js
|
||||
src/generated/*
|
||||
|
@ -3811,9 +3811,6 @@ When all steps combined have not finished during the specified [`option: timeout
|
||||
|
||||
Returns the `node.textContent`.
|
||||
|
||||
## method: ElementHandle.toString
|
||||
- returns: <[string]>
|
||||
|
||||
## async method: ElementHandle.type
|
||||
|
||||
Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.
|
||||
|
@ -3001,7 +3001,6 @@ ElementHandle instances can be used as an argument in [`page.$eval(selector, pag
|
||||
- [elementHandle.setInputFiles(files[, options])](#elementhandlesetinputfilesfiles-options)
|
||||
- [elementHandle.tap([options])](#elementhandletapoptions)
|
||||
- [elementHandle.textContent()](#elementhandletextcontent)
|
||||
- [elementHandle.toString()](#elementhandletostring)
|
||||
- [elementHandle.type(text[, options])](#elementhandletypetext-options)
|
||||
- [elementHandle.uncheck([options])](#elementhandleuncheckoptions)
|
||||
- [elementHandle.waitForElementState(state[, options])](#elementhandlewaitforelementstatestate-options)
|
||||
@ -3375,9 +3374,6 @@ When all steps combined have not finished during the specified `timeout`, this m
|
||||
|
||||
Returns the `node.textContent`.
|
||||
|
||||
#### elementHandle.toString()
|
||||
- returns: <[string]>
|
||||
|
||||
#### elementHandle.type(text[, options])
|
||||
- `text` <[string]> A text to type into a focused element.
|
||||
- `options` <[Object]>
|
||||
|
@ -18,7 +18,7 @@
|
||||
"tsc": "tsc -p .",
|
||||
"tsc-installer": "tsc -p ./src/install/tsconfig.json",
|
||||
"doc": "node utils/doclint/cli.js",
|
||||
"test-infra": "folio utils/doclint/check_public_api/test/test.js && folio utils/doclint/preprocessor/test.js",
|
||||
"test-infra": "folio utils/doclint/check_public_api/test/testMissingDocs.js && folio utils/doclint/preprocessor/test.js",
|
||||
"lint": "npm run eslint && npm run tsc && npm run doc && npm run check-deps && npm run generate-channels && node utils/generate_types/ --check-clean && npm run test-types && npm run test-infra",
|
||||
"clean": "rimraf lib && rimraf types",
|
||||
"prepare": "node install-from-github.js",
|
||||
|
2
types/types.d.ts
vendored
2
types/types.d.ts
vendored
@ -5120,8 +5120,6 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
|
||||
*/
|
||||
textContent(): Promise<null|string>;
|
||||
|
||||
toString(): string;
|
||||
|
||||
/**
|
||||
* Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.
|
||||
*
|
||||
|
@ -1,314 +0,0 @@
|
||||
/**
|
||||
* Copyright 2019 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 path = require('path');
|
||||
const Documentation = require('./Documentation');
|
||||
const EventEmitter = require('events');
|
||||
module.exports = { checkSources };
|
||||
|
||||
/**
|
||||
* @param {!Array<!import('../Source')>} sources
|
||||
*/
|
||||
function checkSources(sources) {
|
||||
// special treatment for Events.js
|
||||
const classEvents = new Map();
|
||||
const eventsSources = sources.filter(source => source.name().startsWith('events.'));
|
||||
for (const eventsSource of eventsSources) {
|
||||
const {Events} = require(eventsSource.filePath().endsWith('.js') ? eventsSource.filePath() : eventsSource.filePath().replace(/\bsrc\b/, 'lib').replace('.ts', '.js'));
|
||||
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.ESNext,
|
||||
strict: true
|
||||
},
|
||||
rootNames: sources.map(source => source.filePath())
|
||||
});
|
||||
const checker = program.getTypeChecker();
|
||||
const sourceFiles = program.getSourceFiles();
|
||||
const errors = [];
|
||||
const apiClassNames = new Set();
|
||||
/** @type {!Array<!Documentation.Class>} */
|
||||
const classes = [];
|
||||
/** @type {!Map<string, string[]>} */
|
||||
const inheritance = new Map();
|
||||
sourceFiles.filter(x => !x.fileName.includes('node_modules')).map(x => visit(x));
|
||||
const documentation = new Documentation(recreateClassesWithInheritance(classes, inheritance).filter(cls => apiClassNames.has(cls.name)));
|
||||
|
||||
return {errors, documentation};
|
||||
|
||||
/**
|
||||
* @param {!Array<!Documentation.Class>} classes
|
||||
* @param {!Map<string, string[]>} inheritance
|
||||
* @return {!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();
|
||||
const visit = cls => {
|
||||
if (!cls)
|
||||
return;
|
||||
for (const member of cls.membersArray) {
|
||||
// Member was overridden.
|
||||
const memberId = member.kind + ':' + member.name;
|
||||
if (membersMap.has(memberId))
|
||||
continue;
|
||||
membersMap.set(memberId, member);
|
||||
}
|
||||
const parents = inheritance.get(cls.name) || [];
|
||||
for (const parent of parents)
|
||||
visit(classesByName.get(parent));
|
||||
};
|
||||
visit(cls);
|
||||
return new Documentation.Class(cls.name, Array.from(membersMap.values()), undefined, undefined, cls.templates);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!ts.Node} node
|
||||
*/
|
||||
function visit(node) {
|
||||
const fileName = node.getSourceFile().fileName;
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node) || ts.isInterfaceDeclaration(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) && !fileName.endsWith('/protocol.ts') && !fileName.endsWith('/protocol.d.ts') && !fileName.endsWith('/types.d.ts') && !fileName.endsWith('node_modules/electron/electron.d.ts')) {
|
||||
excludeClasses.add(className);
|
||||
classes.push(serializeClass(className, symbol, node));
|
||||
inheritance.set(className, parentClasses(node));
|
||||
}
|
||||
}
|
||||
if (fileName.endsWith('/api.ts') && ts.isExportSpecifier(node))
|
||||
apiClassNames.add((node.propertyName || node.name).text);
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
function parentClasses(classNode) {
|
||||
const parents = [];
|
||||
for (const herigateClause of classNode.heritageClauses || []) {
|
||||
for (const heritageType of herigateClause.types) {
|
||||
let expression = heritageType.expression;
|
||||
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression)
|
||||
expression = expression.name;
|
||||
if (classNode.name.escapedText !== expression.escapedText)
|
||||
parents.push(expression.escapedText);
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ts.Symbol} symbol
|
||||
* @param {string[]=} circular
|
||||
* @param {boolean=} parentRequired
|
||||
*/
|
||||
function serializeSymbol(symbol, circular = [], parentRequired = true) {
|
||||
const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
|
||||
const name = symbol.getName();
|
||||
if (symbol.valueDeclaration && symbol.valueDeclaration.dotDotDotToken) {
|
||||
const innerType = serializeType(type.typeArguments ? type.typeArguments[0] : type, circular);
|
||||
innerType.name = '...' + innerType.name;
|
||||
const required = false;
|
||||
return Documentation.Member.createProperty('...' + name, innerType, undefined, required);
|
||||
}
|
||||
|
||||
const required = parentRequired && !typeHasUndefined(type);
|
||||
return Documentation.Member.createProperty(name, serializeType(type, circular), undefined, required);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!ts.Type} type
|
||||
*/
|
||||
function typeHasUndefined(type) {
|
||||
if (!type.isUnion())
|
||||
return type.flags & ts.TypeFlags.Undefined;
|
||||
return type.types.some(typeHasUndefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!ts.Type} type
|
||||
*/
|
||||
function isNotUndefined(type) {
|
||||
return !(type.flags & ts.TypeFlags.Undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.isUnion())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!ts.Type} type
|
||||
* @return {!Documentation.Type}
|
||||
*/
|
||||
function serializeType(type, circular = []) {
|
||||
let typeName = checker.typeToString(type).replace(/SmartHandle/g, 'Handle');
|
||||
if (typeName === 'any')
|
||||
typeName = 'Object';
|
||||
const nextCircular = [typeName].concat(circular);
|
||||
const stringIndexType = type.getStringIndexType();
|
||||
if (stringIndexType) {
|
||||
return new Documentation.Type(`Object<string, ${serializeType(stringIndexType, circular).name}>`);
|
||||
} else if (isRegularObject(type)) {
|
||||
let properties = undefined;
|
||||
if (!circular.includes(typeName))
|
||||
properties = getTypeProperties(type).map(property => serializeSymbol(property, nextCircular));
|
||||
return new Documentation.Type('Object', properties);
|
||||
}
|
||||
if (type.isUnion() && (typeName.includes('|') || type.types.every(type => type.isStringLiteral() || type.intrinsicName === 'number'))) {
|
||||
const types = type.types.filter(isNotUndefined).map((type, index) => {
|
||||
return { isLiteral: type.isStringLiteral(), serialized: serializeType(type, circular), index };
|
||||
});
|
||||
types.sort((a, b) => {
|
||||
if (!a.isLiteral || !b.isLiteral)
|
||||
return a.index - b.index;
|
||||
return a.serialized.name.localeCompare(b.serialized.name);
|
||||
});
|
||||
const name = types.map(type => type.serialized.name).join('|');
|
||||
const properties = [].concat(...types.map(type => type.serialized.properties));
|
||||
return new Documentation.Type(name.replace(/false\|true/g, 'boolean'), properties);
|
||||
}
|
||||
if (type.typeArguments && type.symbol) {
|
||||
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 === 'Promise' ? 'Promise<void>' : type.symbol.name);
|
||||
return new Documentation.Type(`${type.symbol.name}<${innerTypeNames.join(', ')}>`, properties);
|
||||
}
|
||||
return new Documentation.Type(typeName, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} className
|
||||
* @param {!ts.Symbol} symbol
|
||||
* @return {}
|
||||
*/
|
||||
function serializeClass(className, symbol, node) {
|
||||
/** @type {!Array<!Documentation.Member>} */
|
||||
const members = classEvents.get(className) || [];
|
||||
const templates = [];
|
||||
for (const [name, member] of symbol.members || []) {
|
||||
if (className === 'Error')
|
||||
continue;
|
||||
if (name.startsWith('_'))
|
||||
continue;
|
||||
if (member.valueDeclaration && ts.getCombinedModifierFlags(member.valueDeclaration) & ts.ModifierFlags.Private)
|
||||
continue;
|
||||
if (EventEmitter.prototype.hasOwnProperty(name))
|
||||
continue;
|
||||
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
|
||||
const signature = signatureForType(memberType);
|
||||
if (member.flags & ts.SymbolFlags.TypeParameter)
|
||||
templates.push(name);
|
||||
else if (signature)
|
||||
members.push(serializeSignature(name, signature));
|
||||
else
|
||||
members.push(serializeProperty(name, memberType));
|
||||
}
|
||||
|
||||
return new Documentation.Class(className, members, undefined, undefined, templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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(isNotUndefined);
|
||||
if (innerTypes.length === 1)
|
||||
return signatureForType(innerTypes[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {!ts.Signature} signature
|
||||
*/
|
||||
function serializeSignature(name, signature) {
|
||||
const minArgumentCount = signature.minArgumentCount || 0;
|
||||
const parameters = signature.parameters.map((s, index) => serializeSymbol(s, [], index < minArgumentCount));
|
||||
const templates = signature.typeParameters ? signature.typeParameters.map(t => t.symbol.name) : [];
|
||||
const returnType = serializeType(signature.getReturnType());
|
||||
return Documentation.Member.createMethod(name, parameters, returnType.name !== 'void' ? returnType : null, undefined, templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {!ts.Type} type
|
||||
*/
|
||||
function serializeProperty(name, type) {
|
||||
return Documentation.Member.createProperty(name, serializeType(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!ts.Type} type
|
||||
*/
|
||||
function getTypeProperties(type) {
|
||||
if (type.aliasSymbol && type.aliasSymbol.escapedName === 'Pick') {
|
||||
const props = getTypeProperties(type.aliasTypeArguments[0]);
|
||||
const pickNames = type.aliasTypeArguments[1].types.map(t => t.value);
|
||||
return props.filter(p => pickNames.includes(p.getName()));
|
||||
}
|
||||
if (!type.isIntersection())
|
||||
return type.getProperties();
|
||||
let props = [];
|
||||
for (const innerType of type.types) {
|
||||
let innerProps = getTypeProperties(innerType);
|
||||
props = props.filter(p => !innerProps.find(e => e.getName() === p.getName()));
|
||||
props = props.filter(p => p.getName() !== '_tracePath' && p.getName() !== '_traceResourcesPath');
|
||||
props.push(...innerProps);
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
@ -1,325 +0,0 @@
|
||||
/**
|
||||
* 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 EXCLUDE_PROPERTIES = new Set([
|
||||
'JSHandle.toString',
|
||||
]);
|
||||
|
||||
/**
|
||||
* @return {!Array<!Message>}
|
||||
*/
|
||||
module.exports = function lint(api, jsSources) {
|
||||
const mdResult = mdBuilder(api, true);
|
||||
const jsResult = jsBuilder.checkSources(jsSources);
|
||||
const jsDocumentation = filterJSDocumentation(jsSources, 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));
|
||||
|
||||
// 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 {!Array<!Source>} jsSources
|
||||
* @param {!Documentation} jsDocumentation
|
||||
* @return {!Documentation}
|
||||
*/
|
||||
function filterJSDocumentation(jsSources, jsDocumentation) {
|
||||
// Filter private classes and methods.
|
||||
const classes = [];
|
||||
for (const cls of jsDocumentation.classesArray) {
|
||||
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
|
||||
* @return {!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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Documentation} actual
|
||||
* @param {!Documentation} expected
|
||||
* @return {!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);
|
||||
for (const className of classesDiff.extra)
|
||||
errors.push(`Documented but not implemented class: ${className}`);
|
||||
for (const className of classesDiff.missing)
|
||||
errors.push(`Implemented but not documented class: ${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)
|
||||
errors.push(`Documented but not implemented method: ${className}.${methodName}()`);
|
||||
for (const methodName of methodDiff.missing)
|
||||
errors.push(`Implemented but not documented method: ${className}.${methodName}()`);
|
||||
|
||||
for (const methodName of methodDiff.equal) {
|
||||
const actualMethod = actualClass.methods.get(methodName);
|
||||
const expectedMethod = expectedClass.methods.get(methodName);
|
||||
const hasActualType = actualMethod.type && actualMethod.type.name !== 'void';
|
||||
const hasExpectedType = expectedMethod.type && expectedMethod.type.name !== 'void';
|
||||
if (hasActualType !== hasExpectedType) {
|
||||
if (hasActualType)
|
||||
errors.push(`Method ${className}.${methodName} has unneeded description of return type: `+ actualMethod.type.name);
|
||||
else if (hasExpectedType)
|
||||
errors.push(`Method ${className}.${methodName} is missing return type description: ` + expectedMethod.type.name);
|
||||
} else if (hasActualType) {
|
||||
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) {
|
||||
const text = [`Method ${className}.${methodName}() fails to describe its parameters:`];
|
||||
for (const arg of argsDiff.missing)
|
||||
text.push(`- Implemented but not documented argument: ${arg}`);
|
||||
for (const arg of argsDiff.extra)
|
||||
text.push(`- Documented but not implemented argument: ${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)
|
||||
errors.push(`Documented but not implemented property: ${className}.${propertyName}`);
|
||||
for (const propertyName of propertyDiff.missing) {
|
||||
if (propertyName === 'T')
|
||||
continue;
|
||||
errors.push(`Implemented but not documented property: ${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(`Documented but not implemented event ${className}: '${eventName}'`);
|
||||
for (const eventName of eventsDiff.missing)
|
||||
errors.push(`Implemented but not documented event ${className}: '${eventName}'`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {!Documentation.Member} actual
|
||||
* @param {!Documentation.Member} expected
|
||||
*/
|
||||
function checkProperty(source, actual, expected) {
|
||||
if (actual.required !== expected.required)
|
||||
errors.push(`${source}: ${actual.name} should be ${expected.required ? 'required' : 'optional'}`);
|
||||
checkType(source + '.' + actual.name, actual.type, expected.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
if (expected.name === 'T' || expected.name.includes('[T]'))
|
||||
return;
|
||||
/** @type {Parameters<typeof String.prototype.replace>[]} */
|
||||
const mdReplacers = [
|
||||
[/\ /g, ''],
|
||||
// We shortcut ? to null|
|
||||
[/\?/g, 'null|'],
|
||||
[/EvaluationArgument/g, 'Object'],
|
||||
];
|
||||
const tsReplacers = [
|
||||
[/\ /g, ''],
|
||||
[/Arg/g, 'Object'],
|
||||
[/ChromiumBrowserContext/g, 'BrowserContext'],
|
||||
[/ElementHandle<[^>]+>/g, 'ElementHandle'],
|
||||
[/Handle<R>/g, 'JSHandle'],
|
||||
[/JSHandle<Object>/g, 'JSHandle'],
|
||||
[/object/g, 'Object'],
|
||||
[/Promise<T>/, 'Promise<Object>'],
|
||||
[/TextendsNode\?ElementHandle:null/, 'null|ElementHandle']
|
||||
]
|
||||
let actualName = actual.name;
|
||||
for (const replacer of mdReplacers)
|
||||
actualName = actualName.replace(...replacer);
|
||||
let expectedName = expected.name;
|
||||
for (const replacer of tsReplacers)
|
||||
expectedName = expectedName.replace(...replacer);
|
||||
if (normalizeType(expectedName) !== normalizeType(actualName))
|
||||
errors.push(`${source} ${actualName} != ${expectedName}`);
|
||||
if (actual.name === 'boolean' || actual.name === 'string')
|
||||
return;
|
||||
const actualPropertiesMap = new Map(actual.properties.map(property => [property.name, property]));
|
||||
const expectedPropertiesMap = new Map(expected.properties.map(property => [property.name, property]));
|
||||
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)
|
||||
checkProperty(source, actualPropertiesMap.get(propertyName), expectedPropertiesMap.get(propertyName));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<string>} actual
|
||||
* @param {!Array<string>} expected
|
||||
* @return {{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];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeType(type) {
|
||||
let nesting = 0;
|
||||
const result = [];
|
||||
let word = '';
|
||||
for (const c of type) {
|
||||
if (c === '<') {
|
||||
++nesting;
|
||||
} else if (c === '>') {
|
||||
--nesting;
|
||||
}
|
||||
if (c === '|' && !nesting) {
|
||||
result.push(word);
|
||||
word = '';
|
||||
} else {
|
||||
word += c;
|
||||
}
|
||||
}
|
||||
if (word)
|
||||
result.push(word);
|
||||
result.sort();
|
||||
return result.join('|');
|
||||
}
|
175
utils/doclint/check_public_api/missingDocs.js
Normal file
175
utils/doclint/check_public_api/missingDocs.js
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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 mdBuilder = require('./MDBuilder');
|
||||
const Message = require('../Message');
|
||||
const ts = require('typescript');
|
||||
const EventEmitter = require('events');
|
||||
const Documentation = require('./Documentation');
|
||||
|
||||
/**
|
||||
* @return {!Array<!Message>}
|
||||
*/
|
||||
module.exports = function lint(api, jsSources, apiFileName) {
|
||||
const documentation = mdBuilder(api, true).documentation;
|
||||
const apiMethods = listMethods(jsSources, apiFileName);
|
||||
const errors = [];
|
||||
for (const [className, methods] of apiMethods) {
|
||||
const docClass = documentation.classes.get(className);
|
||||
if (!docClass) {
|
||||
errors.push(Message.error(`Missing documentation for "${className}"`));
|
||||
continue;
|
||||
}
|
||||
for (const [methodName, params] of methods) {
|
||||
const member = docClass.members.get(methodName);
|
||||
if (!member) {
|
||||
errors.push(Message.error(`Missing documentation for "${className}.${methodName}"`));
|
||||
continue;
|
||||
}
|
||||
const memberParams = paramsForMember(member);
|
||||
for (const paramName of params) {
|
||||
if (!memberParams.has(paramName))
|
||||
errors.push(Message.error(`Missing documentation for "${className}.${methodName}.${paramName}"`));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const cls of documentation.classesArray) {
|
||||
const methods = apiMethods.get(cls.name);
|
||||
if (!methods) {
|
||||
errors.push(Message.error(`Documented "${cls.name}" not found in sources`));
|
||||
continue;
|
||||
}
|
||||
for (const member of cls.membersArray) {
|
||||
if (member.kind === 'event')
|
||||
continue;
|
||||
const params = methods.get(member.name);
|
||||
if (!params) {
|
||||
errors.push(Message.error(`Documented "${cls.name}.${member.name}" not found is sources`));
|
||||
continue;
|
||||
}
|
||||
const memberParams = paramsForMember(member);
|
||||
for (const paramName of memberParams) {
|
||||
if (!params.has(paramName))
|
||||
errors.push(Message.error(`Documented "${cls.name}.${member.name}.${paramName}" not found is sources`));
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!Documentation.Member} member
|
||||
*/
|
||||
function paramsForMember(member) {
|
||||
if (member.kind !== 'method')
|
||||
return [];
|
||||
const paramNames = new Set(member.argsArray.map(a => a.name));
|
||||
if (member.options)
|
||||
paramNames.add('options');
|
||||
return paramNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<!import('../Source')>} sources
|
||||
*/
|
||||
function listMethods(sources, apiFileName) {
|
||||
const program = ts.createProgram({
|
||||
options: {
|
||||
allowJs: true,
|
||||
target: ts.ScriptTarget.ESNext,
|
||||
strict: true
|
||||
},
|
||||
rootNames: sources.map(source => source.filePath())
|
||||
});
|
||||
const checker = program.getTypeChecker();
|
||||
const apiClassNames = new Set();
|
||||
const apiMethods = new Map();
|
||||
const apiSource = program.getSourceFiles().find(f => f.fileName === apiFileName);
|
||||
|
||||
/**
|
||||
* @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 {!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 classType.symbol.members || []) {
|
||||
if (name.startsWith('_') || name === 'T' || name === 'toString')
|
||||
continue;
|
||||
if (EventEmitter.prototype.hasOwnProperty(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) : 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;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
result-actual.txt
|
||||
result-diff.html
|
@ -1,13 +0,0 @@
|
||||
class Foo {
|
||||
test() {
|
||||
}
|
||||
|
||||
title(arg: number) {
|
||||
}
|
||||
}
|
||||
|
||||
class Bar {
|
||||
}
|
||||
|
||||
export {Bar};
|
||||
export {Foo};
|
@ -1,17 +0,0 @@
|
||||
# class: Bar
|
||||
|
||||
# class: Foo
|
||||
|
||||
## method: Foo.test
|
||||
|
||||
## method: Foo.test
|
||||
|
||||
## method: Foo.title
|
||||
|
||||
### param: Foo.title.arg
|
||||
- `arg` <[number]>
|
||||
|
||||
### param: Foo.title.arg
|
||||
- `arg` <[number]>
|
||||
|
||||
# class: Bar
|
@ -1,3 +0,0 @@
|
||||
[MarkDown] Duplicate declaration of method Foo.test()
|
||||
[MarkDown] Duplicate declaration of argument Foo.title "arg"
|
||||
[MarkDown] Duplicate declaration of class Bar
|
@ -1,19 +0,0 @@
|
||||
class Foo {
|
||||
bar(options?: {x?: number, y?: number, maybe?: number, nullable?: string|null, object?: {one: number, two?: number}}) {
|
||||
|
||||
}
|
||||
|
||||
async goBack() : Promise<Response | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
response(): Response | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
baz(): {abc: number, def?: number, ghi: string} | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export {Foo};
|
@ -1,33 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
## method: Foo.bar
|
||||
|
||||
### option: Foo.bar.x
|
||||
- `x` <[number]>
|
||||
|
||||
### option: Foo.bar.y
|
||||
- `y` <[number]>
|
||||
|
||||
### option: Foo.bar.nullable
|
||||
- `nullable` <?[string]>
|
||||
|
||||
### option: Foo.bar.maybe
|
||||
- `maybe` <[number]>
|
||||
|
||||
### option: Foo.bar.object
|
||||
- `object` <[Object]>
|
||||
- `one` <[number]>
|
||||
- `two` <[number]> defaults to `2`.
|
||||
|
||||
## method: Foo.baz
|
||||
- returns: <?[Object]>
|
||||
- `abc` <[number]>
|
||||
- `def` <[number]> if applicable.
|
||||
- `ghi` <[string]>
|
||||
|
||||
## method: Foo.goBack
|
||||
- returns: <[Promise]<?[Response]>> Promise which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect. If
|
||||
can not go back, resolves to `null`.
|
||||
|
||||
## method: Foo.response
|
||||
- returns: <?[Response]> A matching [Response] object, or `null` if the response has not been received yet.
|
@ -1,21 +0,0 @@
|
||||
class Foo {
|
||||
return42() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
returnNothing() {
|
||||
let e = () => {
|
||||
return 10;
|
||||
}
|
||||
e();
|
||||
}
|
||||
|
||||
www() : string {
|
||||
return 'df';
|
||||
}
|
||||
|
||||
async asyncFunction() {
|
||||
}
|
||||
}
|
||||
|
||||
export {Foo};
|
@ -1,11 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
## async method: Foo.asyncFunction
|
||||
|
||||
## method: Foo.return42
|
||||
|
||||
## method: Foo.returnNothing
|
||||
- returns: <[number]>
|
||||
|
||||
## method: Foo.www
|
||||
- returns: <[string]>
|
@ -1,2 +0,0 @@
|
||||
[MarkDown] Method Foo.return42 is missing return type description: number
|
||||
[MarkDown] Method Foo.returnNothing has unneeded description of return type: number
|
@ -1,11 +0,0 @@
|
||||
class Foo {
|
||||
ddd = 10;
|
||||
|
||||
aaa() {}
|
||||
|
||||
bbb() {}
|
||||
|
||||
ccc() {}
|
||||
}
|
||||
|
||||
export {Foo};
|
@ -1,15 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
## event: Foo.c
|
||||
|
||||
## event: Foo.a
|
||||
|
||||
## method: Foo.aaa
|
||||
|
||||
## event: Foo.b
|
||||
|
||||
## property: Foo.ddd
|
||||
|
||||
## method: Foo.ccc
|
||||
|
||||
## method: Foo.bbb
|
@ -1,8 +0,0 @@
|
||||
const Events = {
|
||||
Foo: {
|
||||
a: 'a',
|
||||
b: 'b',
|
||||
c: 'c',
|
||||
},
|
||||
};
|
||||
module.exports = {Events};
|
@ -1,4 +0,0 @@
|
||||
[MarkDown] Events should go first. Event 'b' in class Foo breaks order
|
||||
[MarkDown] Event 'c' in class Foo breaks alphabetic ordering of events
|
||||
[MarkDown] Bad alphabetic ordering of Foo members: Foo.ddd should go after Foo.ccc()
|
||||
[MarkDown] Bad alphabetic ordering of Foo members: Foo.ccc() should go after Foo.bbb()
|
@ -1,11 +0,0 @@
|
||||
class Foo {
|
||||
foo(arg1: string, arg3 = {}) {
|
||||
}
|
||||
|
||||
test(filePaths : string[]) {
|
||||
}
|
||||
|
||||
bar(options?: {visibility?: boolean}) {
|
||||
}
|
||||
}
|
||||
export {Foo};
|
@ -1,19 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
## method: Foo.bar
|
||||
|
||||
### option: Foo.bar.visibility
|
||||
- `visibility` <[boolean]>
|
||||
|
||||
## method: Foo.foo
|
||||
|
||||
### param: Foo.foo.arg1
|
||||
- `arg1` <[string]>
|
||||
|
||||
### param: Foo.foo.arg2
|
||||
- `arg2` <[string]>
|
||||
|
||||
## method: Foo.test
|
||||
|
||||
### param: Foo.test.filePaths
|
||||
- `filePaths` <[Array]<[string]>>
|
@ -1,3 +0,0 @@
|
||||
[MarkDown] Method Foo.foo() fails to describe its parameters:
|
||||
- Implemented but not documented argument: arg3
|
||||
- Documented but not implemented argument: arg2
|
@ -1,2 +0,0 @@
|
||||
export {Foo} from './foo';
|
||||
export {Other} from './other';
|
@ -1,5 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
# class: Bar
|
||||
|
||||
# class: Baz
|
@ -1,2 +0,0 @@
|
||||
export class Foo {
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export class Other {
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
[MarkDown] Documented but not implemented class: Bar
|
||||
[MarkDown] Documented but not implemented class: Baz
|
||||
[MarkDown] Implemented but not documented class: Other
|
@ -1,4 +0,0 @@
|
||||
class Foo {
|
||||
}
|
||||
|
||||
export {Foo};
|
@ -1,5 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
## event: Foo.start
|
||||
|
||||
## event: Foo.stop
|
@ -1,8 +0,0 @@
|
||||
const Events = {
|
||||
Foo: {
|
||||
Start: 'start',
|
||||
Finish: 'finish',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {Events};
|
@ -1,2 +0,0 @@
|
||||
[MarkDown] Documented but not implemented event Foo: 'stop'
|
||||
[MarkDown] Implemented but not documented event Foo: 'finish'
|
@ -1,18 +0,0 @@
|
||||
class Foo {
|
||||
start() {
|
||||
}
|
||||
|
||||
stop() {
|
||||
}
|
||||
|
||||
get zzz() {
|
||||
}
|
||||
|
||||
$() {
|
||||
}
|
||||
|
||||
money$$money() {
|
||||
}
|
||||
}
|
||||
|
||||
export {Foo};
|
@ -1,10 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
## method: Foo.$
|
||||
|
||||
## method: Foo.money$$money
|
||||
|
||||
## method: Foo.proceed
|
||||
|
||||
## method: Foo.start
|
||||
|
@ -1,3 +0,0 @@
|
||||
[MarkDown] Documented but not implemented method: Foo.proceed()
|
||||
[MarkDown] Implemented but not documented method: Foo.stop()
|
||||
[MarkDown] Implemented but not documented property: Foo.zzz
|
@ -1,5 +0,0 @@
|
||||
class Foo {
|
||||
a = 42;
|
||||
b = 'hello';
|
||||
}
|
||||
export {Foo};
|
@ -1,5 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
## property: Foo.a
|
||||
|
||||
## property: Foo.c
|
@ -1,2 +0,0 @@
|
||||
[MarkDown] Documented but not implemented property: Foo.c
|
||||
[MarkDown] Implemented but not documented property: Foo.b
|
@ -1,15 +0,0 @@
|
||||
class A {
|
||||
property1 = 1;
|
||||
_property2 = 2;
|
||||
constructor(delegate) {
|
||||
}
|
||||
|
||||
get getter() : any {
|
||||
return null;
|
||||
}
|
||||
|
||||
async method(foo, bar) {
|
||||
}
|
||||
}
|
||||
|
||||
export {A};
|
@ -1,6 +0,0 @@
|
||||
const Events = {
|
||||
A: {
|
||||
AnEvent: 'anevent'
|
||||
},
|
||||
};
|
||||
module.exports = { Events };
|
@ -1,50 +0,0 @@
|
||||
{
|
||||
"classes": [
|
||||
{
|
||||
"name": "A",
|
||||
"members": [
|
||||
{
|
||||
"name": "anevent",
|
||||
"kind": "event"
|
||||
},
|
||||
{
|
||||
"name": "property1",
|
||||
"type": {
|
||||
"name": "number"
|
||||
},
|
||||
"kind": "property"
|
||||
},
|
||||
{
|
||||
"name": "getter",
|
||||
"type": {
|
||||
"name": "Object"
|
||||
},
|
||||
"kind": "property"
|
||||
},
|
||||
{
|
||||
"name": "method",
|
||||
"type": {
|
||||
"name": "Promise<void>"
|
||||
},
|
||||
"kind": "method",
|
||||
"args": [
|
||||
{
|
||||
"name": "foo",
|
||||
"type": {
|
||||
"name": "Object"
|
||||
},
|
||||
"kind": "property"
|
||||
},
|
||||
{
|
||||
"name": "bar",
|
||||
"type": {
|
||||
"name": "Object"
|
||||
},
|
||||
"kind": "property"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
class A {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
foo(a) {
|
||||
}
|
||||
|
||||
bar() {
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
bar(override) {
|
||||
}
|
||||
}
|
||||
|
||||
export {A};
|
||||
export {B};
|
@ -1,8 +0,0 @@
|
||||
const Events = {
|
||||
B: {
|
||||
// Event with the same name as a super class method.
|
||||
foo: 'foo',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {Events};
|
@ -1,61 +0,0 @@
|
||||
{
|
||||
"classes": [
|
||||
{
|
||||
"name": "A",
|
||||
"members": [
|
||||
{
|
||||
"name": "foo",
|
||||
"kind": "method",
|
||||
"args": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "Object"
|
||||
},
|
||||
"kind": "property"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bar",
|
||||
"kind": "method"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "B",
|
||||
"members": [
|
||||
{
|
||||
"name": "foo",
|
||||
"kind": "event"
|
||||
},
|
||||
{
|
||||
"name": "bar",
|
||||
"kind": "method",
|
||||
"args": [
|
||||
{
|
||||
"name": "override",
|
||||
"type": {
|
||||
"name": "Object"
|
||||
},
|
||||
"kind": "property"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "foo",
|
||||
"kind": "method",
|
||||
"args": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": {
|
||||
"name": "Object"
|
||||
},
|
||||
"kind": "property"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
## method: Foo.method
|
||||
- returns: <[Promise]<[ElementHandle]>>
|
||||
|
||||
The method does something.
|
||||
|
||||
### param: Foo.method.arg1
|
||||
- `arg1` <[string]>
|
||||
|
||||
A single line argument comment
|
||||
|
||||
### param: Foo.method.arg2
|
||||
- `arg2` <[string]>
|
||||
|
||||
A multiline argument comment:
|
||||
* it could be this
|
||||
* or it could be that
|
@ -1,35 +0,0 @@
|
||||
{
|
||||
"classes": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"members": [
|
||||
{
|
||||
"name": "method",
|
||||
"type": {
|
||||
"name": "Promise<ElementHandle>"
|
||||
},
|
||||
"kind": "method",
|
||||
"comment": "The method does something.",
|
||||
"args": [
|
||||
{
|
||||
"name": "arg1",
|
||||
"type": {
|
||||
"name": "string"
|
||||
},
|
||||
"kind": "property",
|
||||
"comment": "A single line argument comment"
|
||||
},
|
||||
{
|
||||
"name": "arg2",
|
||||
"type": {
|
||||
"name": "string"
|
||||
},
|
||||
"kind": "property",
|
||||
"comment": "A multiline argument comment:\n- it could be this\n- or it could be that"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
# class: Foo
|
||||
|
||||
This is a class.
|
||||
|
||||
## event: Foo.frame
|
||||
- type: <[Frame]>
|
||||
|
||||
This event is dispatched.
|
||||
|
||||
## method: Foo.$
|
||||
- returns: <[Promise]<[ElementHandle]>>
|
||||
|
||||
The method runs document.querySelector.
|
||||
|
||||
### param: Foo.$.selector
|
||||
- `selector` <[string]>
|
||||
|
||||
A selector to query page for
|
||||
|
||||
## property: Foo.url
|
||||
- type: <[string]>
|
||||
|
||||
Contains the URL of the request.
|
@ -1,44 +0,0 @@
|
||||
{
|
||||
"classes": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"comment": "This is a class.",
|
||||
"members": [
|
||||
{
|
||||
"name": "frame",
|
||||
"type": {
|
||||
"name": "Frame"
|
||||
},
|
||||
"kind": "event",
|
||||
"comment": "This event is dispatched."
|
||||
},
|
||||
{
|
||||
"name": "$",
|
||||
"type": {
|
||||
"name": "Promise<ElementHandle>"
|
||||
},
|
||||
"kind": "method",
|
||||
"comment": "The method runs document.querySelector.",
|
||||
"args": [
|
||||
{
|
||||
"name": "selector",
|
||||
"type": {
|
||||
"name": "string"
|
||||
},
|
||||
"kind": "property",
|
||||
"comment": "A selector to query page for"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"type": {
|
||||
"name": "string"
|
||||
},
|
||||
"kind": "property",
|
||||
"comment": "Contains the URL of the request."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
35
utils/doclint/check_public_api/test/test-api-class.ts
Normal file
35
utils/doclint/check_public_api/test/test-api-class.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export class Exists {
|
||||
exists(exists: boolean) {
|
||||
return true;
|
||||
}
|
||||
|
||||
exists2(extra: boolean, options: {}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
extra() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class Extra {
|
||||
exists() {
|
||||
return true;
|
||||
}
|
||||
}
|
20
utils/doclint/check_public_api/test/test-api.md
Normal file
20
utils/doclint/check_public_api/test/test-api.md
Normal file
@ -0,0 +1,20 @@
|
||||
# class: Exists
|
||||
|
||||
## method: Exists.exists
|
||||
|
||||
### param: Exists.exists.exists
|
||||
- `exists` <[boolean]>
|
||||
|
||||
### param: Exists.exists.doesNotExist
|
||||
- `doesNotExist` <[boolean]>
|
||||
|
||||
### option: Exists.exists.option
|
||||
- `option` <[number]>
|
||||
|
||||
## method: Exists.exists2
|
||||
|
||||
## method: Exists.doesNotExist
|
||||
|
||||
# class: DoesNotExist
|
||||
|
||||
## method: DoesNotExist.doesNotExist
|
18
utils/doclint/check_public_api/test/test-api.ts
Normal file
18
utils/doclint/check_public_api/test/test-api.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { Exists } from './test-api-class';
|
||||
export { Extra } from './test-api-class';
|
@ -1,111 +0,0 @@
|
||||
/**
|
||||
* 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 fs = require('fs');
|
||||
const path = require('path');
|
||||
const checkPublicAPI = require('..');
|
||||
const Source = require('../../Source');
|
||||
const mdBuilder = require('../MDBuilder');
|
||||
const jsBuilder = require('../JSBuilder');
|
||||
const { folio } = require('folio');
|
||||
const { parseMd } = require('../../../parse_md');
|
||||
|
||||
const fixtures = folio.extend();
|
||||
const { describe, it, expect } = fixtures.build();
|
||||
|
||||
describe('checkPublicAPI', function() {
|
||||
testLint('diff-classes');
|
||||
testLint('diff-methods');
|
||||
testLint('diff-properties');
|
||||
testLint('diff-arguments');
|
||||
testLint('diff-events');
|
||||
testLint('check-duplicates');
|
||||
testLint('check-sorting');
|
||||
testLint('check-returns');
|
||||
testLint('check-nullish');
|
||||
testJSBuilder('js-builder-common');
|
||||
testJSBuilder('js-builder-inheritance');
|
||||
testMDBuilder('md-builder-common');
|
||||
testMDBuilder('md-builder-comments');
|
||||
});
|
||||
|
||||
async function testLint(name) {
|
||||
it(name, async({}) => {
|
||||
const dirPath = path.join(__dirname, name);
|
||||
const api = parseMd(fs.readFileSync(path.join(dirPath, 'doc.md')).toString());
|
||||
const tsSources = await Source.readdir(dirPath, '.ts');
|
||||
const jsSources = await Source.readdir(dirPath, '.js');
|
||||
const messages = await checkPublicAPI(api, jsSources.concat(tsSources));
|
||||
const errors = messages.map(message => message.text);
|
||||
expect(errors.join('\n')).toBe(fs.readFileSync(path.join(dirPath, 'result.txt')).toString());
|
||||
});
|
||||
}
|
||||
|
||||
async function testMDBuilder(name) {
|
||||
it(name, ({}) => {
|
||||
const dirPath = path.join(__dirname, name);
|
||||
const api = parseMd(fs.readFileSync(path.join(dirPath, 'doc.md')).toString());
|
||||
const {documentation} = mdBuilder(api, true);
|
||||
expect(serialize(documentation)).toBe(fs.readFileSync(path.join(dirPath, 'result.txt')).toString());
|
||||
});
|
||||
}
|
||||
|
||||
async function testJSBuilder(name) {
|
||||
it(name, async() => {
|
||||
const dirPath = path.join(__dirname, name);
|
||||
const jsSources = await Source.readdir(dirPath, '.js');
|
||||
const tsSources = await Source.readdir(dirPath, '.ts');
|
||||
const {documentation} = await jsBuilder.checkSources(jsSources.concat(tsSources));
|
||||
expect(serialize(documentation)).toBe(fs.readFileSync(path.join(dirPath, 'result.txt')).toString());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../Documentation')} doc
|
||||
*/
|
||||
function serialize(doc) {
|
||||
const result = {
|
||||
classes: doc.classesArray.map(cls => ({
|
||||
name: cls.name,
|
||||
comment: cls.comment || undefined,
|
||||
members: cls.membersArray.map(serializeMember)
|
||||
}))
|
||||
};
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
/**
|
||||
* @param {import('../Documentation').Member} member
|
||||
*/
|
||||
function serializeMember(member) {
|
||||
return {
|
||||
name: member.name,
|
||||
type: serializeType(member.type),
|
||||
kind: member.kind,
|
||||
comment: member.comment || undefined,
|
||||
args: member.argsArray.length ? member.argsArray.map(serializeMember) : undefined
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {import('../Documentation').Type} type
|
||||
*/
|
||||
function serializeType(type) {
|
||||
if (!type)
|
||||
return undefined;
|
||||
return {
|
||||
name: type.name,
|
||||
properties: type.properties.length ? type.properties.map(serializeMember) : undefined
|
||||
}
|
||||
}
|
45
utils/doclint/check_public_api/test/testMissingDocs.js
Normal file
45
utils/doclint/check_public_api/test/testMissingDocs.js
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 fs = require('fs');
|
||||
const path = require('path');
|
||||
const missingDocs = require('../missingDocs');
|
||||
const Source = require('../../Source');
|
||||
const { folio } = require('folio');
|
||||
const { parseMd } = require('../../../parse_md');
|
||||
|
||||
const { test, expect } = folio;
|
||||
|
||||
test('missing docs', async ({}) => {
|
||||
const api = parseMd(fs.readFileSync(path.join(__dirname, 'test-api.md')).toString());
|
||||
const tsSources = [
|
||||
await Source.readFile(path.join(__dirname, 'test-api.ts')),
|
||||
await Source.readFile(path.join(__dirname, 'test-api-class.ts')),
|
||||
];
|
||||
const messages = missingDocs(api, tsSources, path.join(__dirname, 'test-api.ts'));
|
||||
const errors = messages.map(message => message.text);
|
||||
expect(errors).toEqual([
|
||||
'Missing documentation for "Exists.exists2.extra"',
|
||||
'Missing documentation for "Exists.exists2.options"',
|
||||
'Missing documentation for "Exists.extra"',
|
||||
'Missing documentation for "Extra"',
|
||||
'Documented "Exists.exists.doesNotExist" not found is sources',
|
||||
'Documented "Exists.exists.options" not found is sources',
|
||||
'Documented "Exists.doesNotExist" not found is sources',
|
||||
'Documented "DoesNotExist" not found in sources',
|
||||
]);
|
||||
});
|
@ -120,7 +120,7 @@ async function run() {
|
||||
result.push({
|
||||
type: 'text',
|
||||
text: links
|
||||
});
|
||||
});
|
||||
api.setText([comment, header, renderMd(result, 10000), footer].join('\n'));
|
||||
}
|
||||
}
|
||||
@ -139,10 +139,9 @@ async function run() {
|
||||
for (const source of mdSources.filter(source => source.hasUpdatedText()))
|
||||
messages.push(Message.warning(`WARN: updated ${source.projectPath()}`));
|
||||
|
||||
const checkPublicAPI = require('./check_public_api');
|
||||
|
||||
const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src', 'client'), '', []);
|
||||
messages.push(...checkPublicAPI(apiSpec, jsSources));
|
||||
const missingDocs = require('./check_public_api/missingDocs.js');
|
||||
messages.push(...missingDocs(apiSpec, jsSources, path.join(PROJECT_DIR, 'src', 'client', 'api.ts')));
|
||||
|
||||
for (const source of mdSources) {
|
||||
if (!source.hasUpdatedText())
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
//@ts-check
|
||||
const path = require('path');
|
||||
const Source = require('../doclint/Source');
|
||||
const {devices} = require('../..');
|
||||
const Documentation = require('../doclint/check_public_api/Documentation');
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||
@ -40,10 +39,14 @@ let hadChanges = false;
|
||||
const apiBody = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-body.md')).toString());
|
||||
const apiParams = parseMd(fs.readFileSync(path.join(PROJECT_DIR, 'docs-src', 'api-params.md')).toString());
|
||||
const api = applyTemplates(apiBody, apiParams);
|
||||
const {documentation: mdDocumentation} = require('../doclint/check_public_api/MDBuilder')(api, true);
|
||||
const sources = await Source.readdir(path.join(PROJECT_DIR, 'src', 'client'), '', []);
|
||||
const {documentation: jsDocumentation} = await require('../doclint/check_public_api/JSBuilder').checkSources(sources);
|
||||
documentation = mergeDocumentation(mdDocumentation, jsDocumentation);
|
||||
const mdResult = require('../doclint/check_public_api/MDBuilder')(api, true);
|
||||
documentation = mdResult.documentation;
|
||||
|
||||
// Root module types are overridden.
|
||||
const playwrightClass = documentation.classes.get('Playwright');
|
||||
documentation.classes.delete('Playwright');
|
||||
documentation.classesArray.splice(documentation.classesArray.indexOf(playwrightClass), 1);
|
||||
|
||||
const handledClasses = new Set();
|
||||
|
||||
function docClassForName(name) {
|
||||
@ -124,8 +127,6 @@ function classToString(classDesc) {
|
||||
if (classDesc.comment) {
|
||||
parts.push(writeComment(classDesc.comment))
|
||||
}
|
||||
if (classDesc.templates.length)
|
||||
console.error(`expected an override for "${classDesc.name}" becasue it is templated`);
|
||||
parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`);
|
||||
parts.push(classBody(classDesc));
|
||||
parts.push('}\n');
|
||||
@ -210,8 +211,6 @@ function classBody(classDesc) {
|
||||
// do this late, because we still want object definitions for overridden types
|
||||
if (!hasOwnMethod(classDesc, member.name))
|
||||
return '';
|
||||
if (member.templates.length)
|
||||
console.error(`expected an override for "${classDesc.name}.${member.name}" because it is templated`);
|
||||
return `${jsdoc}${member.name}${args}: ${type};`
|
||||
}).filter(x => x).join('\n\n'));
|
||||
return parts.join('\n');
|
||||
@ -414,27 +413,6 @@ function memberJSDOC(member, indent) {
|
||||
return writeComment(lines.join('\n'), indent) + '\n' + indent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation} mdDoc
|
||||
* @param {Documentation} jsDoc
|
||||
* @return {Documentation}
|
||||
*/
|
||||
function mergeDocumentation(mdDoc, jsDoc) {
|
||||
const classes = [];
|
||||
for (const mdClass of mdDoc.classesArray) {
|
||||
const jsClass = jsDoc.classes.get(mdClass.name);
|
||||
if (!jsClass)
|
||||
classes.push(mdClass);
|
||||
else
|
||||
classes.push(mergeClasses(mdClass, jsClass));
|
||||
}
|
||||
// Root module types are overridden.
|
||||
const c = mdDoc.classes.get('Playwright');
|
||||
mdDoc.classes.delete('Playwright');
|
||||
mdDoc.classesArray.splice(mdDoc.classesArray.indexOf(c), 1);
|
||||
return mdDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Class} mdClass
|
||||
* @param {Documentation.Class} jsClass
|
||||
|
@ -26,7 +26,8 @@ process.on('exit', () => spawns.forEach(s => s.kill()));
|
||||
|
||||
runOnChanges(['src/protocol/protocol.yml'], 'utils/generate_channels.js');
|
||||
runOnChanges([
|
||||
'docs/api.md',
|
||||
'docs-src/api-body.md',
|
||||
'docs-src/api-params.md',
|
||||
'utils/generate_types/overrides.d.ts',
|
||||
'utils/generate_types/exported.json',
|
||||
'src/server/chromium/protocol.ts',
|
||||
|
Loading…
Reference in New Issue
Block a user