mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			184 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			184 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * Copyright 2017 Google Inc. All rights reserved.
 | |
|  * Modifications copyright (c) Microsoft Corporation.
 | |
|  *
 | |
|  * Licensed under the Apache License, Version 2.0 (the "License");
 | |
|  * you may not use this file except in compliance with the License.
 | |
|  * You may obtain a copy of the License at
 | |
|  *
 | |
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | |
|  *
 | |
|  * Unless required by applicable law or agreed to in writing, software
 | |
|  * distributed under the License is distributed on an "AS IS" BASIS,
 | |
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|  * See the License for the specific language governing permissions and
 | |
|  * limitations under the License.
 | |
|  */
 | |
| 
 | |
| const ts = require('typescript');
 | |
| const EventEmitter = require('events');
 | |
| const Documentation = require('./documentation');
 | |
| const path = require('path');
 | |
| 
 | |
| /** @typedef {import('../../markdown').MarkdownNode} MarkdownNode */
 | |
| 
 | |
| const IGNORE_CLASSES = ['PlaywrightAssertions', 'LocatorAssertions', 'PageAssertions', 'APIResponseAssertions', 'SnapshotAssertions'];
 | |
| 
 | |
| module.exports = function lint(documentation, jsSources, apiFileName) {
 | |
|   const errors = [];
 | |
|   documentation.copyDocsFromSuperclasses(errors);
 | |
|   const apiMethods = listMethods(jsSources, apiFileName);
 | |
|   for (const [className, methods] of apiMethods) {
 | |
|     const docClass = documentation.classes.get(className);
 | |
|     if (!docClass) {
 | |
|       errors.push(`Missing documentation for "${className}"`);
 | |
|       continue;
 | |
|     }
 | |
|     for (const [methodName, params] of methods) {
 | |
|       const member = docClass.membersArray.find(m => m.alias === methodName && m.kind !== 'event');
 | |
|       if (!member) {
 | |
|         errors.push(`Missing documentation for "${className}.${methodName}"`);
 | |
|         continue;
 | |
|       }
 | |
|       const memberParams = paramsForMember(member);
 | |
|       for (const paramName of params) {
 | |
|         if (!memberParams.has(paramName))
 | |
|           errors.push(`Missing documentation for "${className}.${methodName}.${paramName}"`);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   for (const cls of documentation.classesArray) {
 | |
|     if (IGNORE_CLASSES.includes(cls.name))
 | |
|       continue;
 | |
|     const methods = apiMethods.get(cls.name);
 | |
|     if (!methods) {
 | |
|       errors.push(`Documented "${cls.name}" not found in sources`);
 | |
|       continue;
 | |
|     }
 | |
|     for (const member of cls.membersArray) {
 | |
|       if (member.kind === 'event')
 | |
|         continue;
 | |
|       const params = methods.get(member.alias);
 | |
|       if (!params) {
 | |
|         errors.push(`Documented "${cls.name}.${member.alias}" not found in sources`);
 | |
|         continue;
 | |
|       }
 | |
|       const memberParams = paramsForMember(member);
 | |
|       for (const paramName of memberParams) {
 | |
|         if (!params.has(paramName) && paramName !== 'options')
 | |
|           errors.push(`Documented "${cls.name}.${member.alias}.${paramName}" not found in sources`);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   return errors;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @param {!Documentation.Member} member
 | |
|  */
 | |
| function paramsForMember(member) {
 | |
|   if (member.kind !== 'method')
 | |
|     return new Set();
 | |
|   return new Set(member.argsArray.map(a => a.alias));
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string[]} rootNames
 | |
|  */
 | |
| function listMethods(rootNames, apiFileName) {
 | |
|   const program = ts.createProgram({
 | |
|     options: {
 | |
|       allowJs: true,
 | |
|       target: ts.ScriptTarget.ESNext,
 | |
|       strict: true
 | |
|     },
 | |
|     rootNames
 | |
|   });
 | |
|   const checker = program.getTypeChecker();
 | |
|   const apiClassNames = new Set();
 | |
|   const apiMethods = new Map();
 | |
|   const apiSource = program.getSourceFiles().find(f => f.fileName === apiFileName.split(path.sep).join(path.posix.sep));
 | |
|   /**
 | |
|    * @param {ts.Type} type
 | |
|    */
 | |
|   function signatureForType(type) {
 | |
|     const signatures = type.getCallSignatures();
 | |
|     if (signatures.length)
 | |
|       return signatures[signatures.length - 1];
 | |
|     if (type.isUnion()) {
 | |
|       const innerTypes = type.types.filter(t => !(t.flags & ts.TypeFlags.Undefined));
 | |
|       if (innerTypes.length === 1)
 | |
|         return signatureForType(innerTypes[0]);
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {string} className
 | |
|    * @param {string} methodName
 | |
|    */
 | |
|   function shouldSkipMethodByName(className, methodName) {
 | |
|     if (methodName.startsWith('_') || methodName === 'T' || methodName === 'toString')
 | |
|       return true;
 | |
|     if (/** @type {any} */(EventEmitter).prototype.hasOwnProperty(methodName))
 | |
|       return true;
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {string} className
 | |
|    * @param {!ts.Type} classType
 | |
|    */
 | |
|   function visitClass(className, classType) {
 | |
|     let methods = apiMethods.get(className);
 | |
|     if (!methods) {
 | |
|       methods = new Map();
 | |
|       apiMethods.set(className, methods);
 | |
|     }
 | |
|     for (const [name, member] of /** @type {any[]} */(classType.symbol.members || [])) {
 | |
|       if (shouldSkipMethodByName(className, name))
 | |
|         continue;
 | |
|       const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
 | |
|       const signature = signatureForType(memberType);
 | |
|       if (signature)
 | |
|         methods.set(name, new Set(signature.parameters.map(p => p.escapedName)));
 | |
|       else
 | |
|         methods.set(name, new Set());
 | |
|     }
 | |
|     for (const baseType of classType.getBaseTypes() || []) {
 | |
|       const baseTypeName = baseType.symbol ? baseType.symbol.name : '';
 | |
|       if (apiClassNames.has(baseTypeName))
 | |
|         visitClass(className, baseType);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {!ts.Node} node
 | |
|    */
 | |
|   function visitMethods(node) {
 | |
|     if (ts.isExportSpecifier(node)) {
 | |
|       const className = node.name.text;
 | |
|       const exportSymbol = node.name ? checker.getSymbolAtLocation(node.name) : /** @type {any} */ (node).symbol;
 | |
|       const classType = checker.getDeclaredTypeOfSymbol(exportSymbol);
 | |
|       if (!classType)
 | |
|         throw new Error(`Cannot parse class "${className}"`);
 | |
|       visitClass(className, classType);
 | |
|     }
 | |
|     ts.forEachChild(node, visitMethods);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {!ts.Node} node
 | |
|    */
 | |
|   function visitNames(node) {
 | |
|     if (ts.isExportSpecifier(node))
 | |
|       apiClassNames.add(node.name.text);
 | |
|     ts.forEachChild(node, visitNames);
 | |
|   }
 | |
| 
 | |
|   visitNames(apiSource);
 | |
|   visitMethods(apiSource);
 | |
| 
 | |
|   return apiMethods;
 | |
| }
 | 
