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:
Dmitry Gozman 2020-12-28 10:54:47 -08:00 committed by GitHub
parent a446792c18
commit 94077e0e74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 307 additions and 1342 deletions

View File

@ -1,5 +1,4 @@
test/assets/modernizr.js
utils/doclint/check_public_api/test/
lib/
*.js
src/generated/*

View File

@ -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.

View File

@ -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]>

View File

@ -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
View File

@ -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.
*

View File

@ -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;
}
}

View File

@ -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('|');
}

View 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;
}

View File

@ -1,2 +0,0 @@
result-actual.txt
result-diff.html

View File

@ -1,13 +0,0 @@
class Foo {
test() {
}
title(arg: number) {
}
}
class Bar {
}
export {Bar};
export {Foo};

View File

@ -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

View File

@ -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

View File

@ -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};

View File

@ -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.

View File

@ -1,21 +0,0 @@
class Foo {
return42() {
return 42;
}
returnNothing() {
let e = () => {
return 10;
}
e();
}
www() : string {
return 'df';
}
async asyncFunction() {
}
}
export {Foo};

View File

@ -1,11 +0,0 @@
# class: Foo
## async method: Foo.asyncFunction
## method: Foo.return42
## method: Foo.returnNothing
- returns: <[number]>
## method: Foo.www
- returns: <[string]>

View File

@ -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

View File

@ -1,11 +0,0 @@
class Foo {
ddd = 10;
aaa() {}
bbb() {}
ccc() {}
}
export {Foo};

View File

@ -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

View File

@ -1,8 +0,0 @@
const Events = {
Foo: {
a: 'a',
b: 'b',
c: 'c',
},
};
module.exports = {Events};

View File

@ -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()

View File

@ -1,11 +0,0 @@
class Foo {
foo(arg1: string, arg3 = {}) {
}
test(filePaths : string[]) {
}
bar(options?: {visibility?: boolean}) {
}
}
export {Foo};

View File

@ -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]>>

View File

@ -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

View File

@ -1,2 +0,0 @@
export {Foo} from './foo';
export {Other} from './other';

View File

@ -1,5 +0,0 @@
# class: Foo
# class: Bar
# class: Baz

View File

@ -1,2 +0,0 @@
export class Foo {
}

View File

@ -1,2 +0,0 @@
export class Other {
}

View File

@ -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

View File

@ -1,4 +0,0 @@
class Foo {
}
export {Foo};

View File

@ -1,5 +0,0 @@
# class: Foo
## event: Foo.start
## event: Foo.stop

View File

@ -1,8 +0,0 @@
const Events = {
Foo: {
Start: 'start',
Finish: 'finish',
},
};
module.exports = {Events};

View File

@ -1,2 +0,0 @@
[MarkDown] Documented but not implemented event Foo: 'stop'
[MarkDown] Implemented but not documented event Foo: 'finish'

View File

@ -1,18 +0,0 @@
class Foo {
start() {
}
stop() {
}
get zzz() {
}
$() {
}
money$$money() {
}
}
export {Foo};

View File

@ -1,10 +0,0 @@
# class: Foo
## method: Foo.$
## method: Foo.money$$money
## method: Foo.proceed
## method: Foo.start

View File

@ -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

View File

@ -1,5 +0,0 @@
class Foo {
a = 42;
b = 'hello';
}
export {Foo};

View File

@ -1,5 +0,0 @@
# class: Foo
## property: Foo.a
## property: Foo.c

View File

@ -1,2 +0,0 @@
[MarkDown] Documented but not implemented property: Foo.c
[MarkDown] Implemented but not documented property: Foo.b

View File

@ -1,15 +0,0 @@
class A {
property1 = 1;
_property2 = 2;
constructor(delegate) {
}
get getter() : any {
return null;
}
async method(foo, bar) {
}
}
export {A};

View File

@ -1,6 +0,0 @@
const Events = {
A: {
AnEvent: 'anevent'
},
};
module.exports = { Events };

View File

@ -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"
}
]
}
]
}
]
}

View File

@ -1,18 +0,0 @@
class A {
constructor() {
}
foo(a) {
}
bar() {
}
}
class B extends A {
bar(override) {
}
}
export {A};
export {B};

View File

@ -1,8 +0,0 @@
const Events = {
B: {
// Event with the same name as a super class method.
foo: 'foo',
},
};
module.exports = {Events};

View File

@ -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"
}
]
}
]
}
]
}

View File

@ -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

View File

@ -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"
}
]
}
]
}
]
}

View File

@ -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.

View File

@ -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."
}
]
}
]
}

View 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;
}
}

View 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

View 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';

View File

@ -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
}
}

View 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',
]);
});

View File

@ -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())

View File

@ -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

View File

@ -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',