| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * 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'); | 
					
						
							| 
									
										
										
										
											2021-01-07 15:00:04 -08:00
										 |  |  | const Documentation = require('./documentation'); | 
					
						
							| 
									
										
										
										
											2021-02-04 05:31:59 -08:00
										 |  |  | const path = require('path'); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-28 17:38:00 -08:00
										 |  |  | /** @typedef {import('../../markdown').MarkdownNode} MarkdownNode */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-03 10:06:14 -08:00
										 |  |  | const IGNORE_CLASSES = ['PlaywrightAssertions', 'LocatorAssertions', 'PageAssertions', 'APIResponseAssertions', 'ScreenshotAssertions']; | 
					
						
							| 
									
										
										
										
											2021-11-24 21:58:35 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-07 15:00:04 -08:00
										 |  |  | module.exports = function lint(documentation, jsSources, apiFileName) { | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |   const errors = []; | 
					
						
							| 
									
										
										
										
											2021-01-07 15:00:04 -08:00
										 |  |  |   documentation.copyDocsFromSuperclasses(errors); | 
					
						
							| 
									
										
										
										
											2020-12-28 17:38:00 -08:00
										 |  |  |   const apiMethods = listMethods(jsSources, apiFileName); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |   for (const [className, methods] of apiMethods) { | 
					
						
							|  |  |  |     const docClass = documentation.classes.get(className); | 
					
						
							|  |  |  |     if (!docClass) { | 
					
						
							| 
									
										
										
										
											2020-12-28 16:19:28 -08:00
										 |  |  |       errors.push(`Missing documentation for "${className}"`); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |       continue; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     for (const [methodName, params] of methods) { | 
					
						
							| 
									
										
										
										
											2021-01-28 17:51:41 -08:00
										 |  |  |       const member = docClass.membersArray.find(m => m.alias === methodName && m.kind !== 'event'); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |       if (!member) { | 
					
						
							| 
									
										
										
										
											2020-12-28 16:19:28 -08:00
										 |  |  |         errors.push(`Missing documentation for "${className}.${methodName}"`); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |         continue; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const memberParams = paramsForMember(member); | 
					
						
							|  |  |  |       for (const paramName of params) { | 
					
						
							|  |  |  |         if (!memberParams.has(paramName)) | 
					
						
							| 
									
										
										
										
											2020-12-28 16:19:28 -08:00
										 |  |  |           errors.push(`Missing documentation for "${className}.${methodName}.${paramName}"`); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   for (const cls of documentation.classesArray) { | 
					
						
							| 
									
										
										
										
											2021-11-24 21:58:35 +01:00
										 |  |  |     if (IGNORE_CLASSES.includes(cls.name)) | 
					
						
							|  |  |  |       continue; | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |     const methods = apiMethods.get(cls.name); | 
					
						
							|  |  |  |     if (!methods) { | 
					
						
							| 
									
										
										
										
											2020-12-28 16:19:28 -08:00
										 |  |  |       errors.push(`Documented "${cls.name}" not found in sources`); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |       continue; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     for (const member of cls.membersArray) { | 
					
						
							|  |  |  |       if (member.kind === 'event') | 
					
						
							|  |  |  |         continue; | 
					
						
							| 
									
										
										
										
											2021-01-28 17:51:41 -08:00
										 |  |  |       const params = methods.get(member.alias); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |       if (!params) { | 
					
						
							| 
									
										
										
										
											2021-09-07 13:27:53 -04:00
										 |  |  |         errors.push(`Documented "${cls.name}.${member.alias}" not found in sources`); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |         continue; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const memberParams = paramsForMember(member); | 
					
						
							|  |  |  |       for (const paramName of memberParams) { | 
					
						
							| 
									
										
										
										
											2021-01-08 15:00:14 -08:00
										 |  |  |         if (!params.has(paramName) && paramName !== 'options') | 
					
						
							| 
									
										
										
										
											2021-09-07 13:27:53 -04:00
										 |  |  |           errors.push(`Documented "${cls.name}.${member.alias}.${paramName}" not found in sources`); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return errors; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {!Documentation.Member} member | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function paramsForMember(member) { | 
					
						
							|  |  |  |   if (member.kind !== 'method') | 
					
						
							| 
									
										
										
										
											2020-12-28 17:38:00 -08:00
										 |  |  |     return new Set(); | 
					
						
							| 
									
										
										
										
											2021-01-28 17:51:41 -08:00
										 |  |  |   return new Set(member.argsArray.map(a => a.alias)); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							| 
									
										
										
										
											2021-01-01 15:17:27 -08:00
										 |  |  |  * @param {string[]} rootNames | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-01 15:17:27 -08:00
										 |  |  | function listMethods(rootNames, apiFileName) { | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |   const program = ts.createProgram({ | 
					
						
							|  |  |  |     options: { | 
					
						
							|  |  |  |       allowJs: true, | 
					
						
							|  |  |  |       target: ts.ScriptTarget.ESNext, | 
					
						
							|  |  |  |       strict: true | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2021-01-01 15:17:27 -08:00
										 |  |  |     rootNames | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |   }); | 
					
						
							|  |  |  |   const checker = program.getTypeChecker(); | 
					
						
							|  |  |  |   const apiClassNames = new Set(); | 
					
						
							|  |  |  |   const apiMethods = new Map(); | 
					
						
							| 
									
										
										
										
											2021-02-04 05:31:59 -08:00
										 |  |  |   const apiSource = program.getSourceFiles().find(f => f.fileName === apiFileName.split(path.sep).join(path.posix.sep)); | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * @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; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-12 21:23:22 +01:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * @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; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * @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); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-12-28 17:38:00 -08:00
										 |  |  |     for (const [name, member] of /** @type {any[]} */(classType.symbol.members || [])) { | 
					
						
							| 
									
										
										
										
											2021-06-12 21:23:22 +01:00
										 |  |  |       if (shouldSkipMethodByName(className, name)) | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |         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; | 
					
						
							| 
									
										
										
										
											2020-12-28 17:38:00 -08:00
										 |  |  |       const exportSymbol = node.name ? checker.getSymbolAtLocation(node.name) : /** @type {any} */ (node).symbol; | 
					
						
							| 
									
										
										
										
											2020-12-28 10:54:47 -08:00
										 |  |  |       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; | 
					
						
							|  |  |  | } |