170 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			170 lines
		
	
	
		
			5.3 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');
 | 
						|
 | 
						|
/** @typedef {import('../../markdown').MarkdownNode} MarkdownNode */
 | 
						|
 | 
						|
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.members.get(methodName);
 | 
						|
      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) {
 | 
						|
    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.name);
 | 
						|
      if (!params) {
 | 
						|
        errors.push(`Documented "${cls.name}.${member.name}" not found is sources`);
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      const memberParams = paramsForMember(member);
 | 
						|
      for (const paramName of memberParams) {
 | 
						|
        if (!params.has(paramName) && paramName !== 'options')
 | 
						|
          errors.push(`Documented "${cls.name}.${member.name}.${paramName}" not found is 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.name));
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @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);
 | 
						|
 | 
						|
  /**
 | 
						|
   * @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 /** @type {any[]} */(classType.symbol.members || [])) {
 | 
						|
      if (name.startsWith('_') || name === 'T' || name === 'toString')
 | 
						|
        continue;
 | 
						|
      if (/** @type {any} */(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) : /** @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;
 | 
						|
}
 |