Home Manual Reference Source Test Repository

src/Publisher/Builder/LintDocBuilder.js

import path from 'path';
import fs from 'fs-extra';
import DocBuilder from './DocBuilder.js';
import ASTNodeContainer from '../../Util/ASTNodeContainer.js';
import InvalidCodeLogger from '../../Util/InvalidCodeLogger.js';

/** @ignore */
export const _results = [];

/**
 * Lint Output Builder class.
 */
export default class LintDocBuilder extends DocBuilder {
  /**
   * execute building output.
   */
  exec() {
    const results = [];
    const docs = this._find({kind: ['method', 'function']});
    for (let doc of docs) {
      if (doc.undocument) continue;

      const node = ASTNodeContainer.getNode(doc.__docId__);
      const codeParams = this._getParamsFromNode(node);
      const docParams = this._getParamsFromDoc(doc);
      if (this._match(codeParams, docParams)) continue;

      results.push({node, doc, codeParams, docParams});
    }

    _results.push(...results);

    this._showResult(results);
  }

  /**
   * get variable names of method argument.
   * @param {ASTNode} node - target node.
   * @returns {string[]} variable names.
   * @private
   */
  _getParamsFromNode(node) {
    let params;
    switch (node.type) {
      case 'FunctionExpression':
      case 'FunctionDeclaration':
        params = node.params || [];
        break;
      case 'MethodDefinition':
        params = node.value.params || [];
        break;
      default:
        throw new Error(`unknown node type. type = ${node.type}`);
    }

    const result = [];
    for (let param of params) {
      switch (param.type) {
        case 'Identifier':
          result.push(param.name);
          break;
        case 'AssignmentPattern':
          if (param.left.type === 'Identifier') {
            result.push(param.left.name);
          } else if(param.left.type === 'ObjectPattern') {
            result.push('*');
          }
          break;
        case 'RestElement':
          result.push(param.argument.name);
          break;
        case 'ObjectPattern':
          result.push('*');
          break;
        case 'ArrayPattern':
          result.push('*');
          break;
      }
    }

    return result;
  }

  /**
   * get variable names of method argument.
   * @param {DocObject} doc - target doc object.
   * @returns {string[]} variable names.
   * @private
   */
  _getParamsFromDoc(doc) {
    const params = doc.params || [];
    return params.map(v => v.name).filter(v => !v.includes('.'));
  }

  _match(codeParams, docParams) {
    if (codeParams.length !== docParams.length) return false;

    for (let i = 0; i < codeParams.length; i++) {
      if (codeParams[i] === '*') {
        // nothing
      } else if (codeParams[i] !== docParams[i]) {
        return false;
      }
    }

    return true;
  }

  /**
   * show invalid lint code.
   * @param {Object[]} results - target results.
   * @param {DocObject} results[].doc
   * @param {ASTNode} results[].node
   * @param {string[]} results[].codeParams
   * @param {string[]} results[].docParams
   * @private
   */
  _showResult(results) {
    const sourceDir = path.dirname(path.resolve(this._config.source));
    for (let result of results) {
      const doc = result.doc;
      const node = result.node;
      const filePath = doc.longname.split('~')[0];
      const name = doc.longname.split('~')[1];
      const absFilePath = path.resolve(sourceDir, filePath);
      const comment = node.leadingComments[node.leadingComments.length - 1];
      const startLineNumber = comment.loc.start.line;
      const endLineNumber = node.loc.start.line;
      const lines = fs.readFileSync(absFilePath).toString().split('\n');
      const targetLines = [];

      for (let i = startLineNumber - 1; i < endLineNumber; i++) {
        targetLines.push(`${i}| ` + lines[i]);
      }

      console.log(`warning: signature mismatch: ${name} ${filePath}#${startLineNumber}`);
      console.log(targetLines.join('\n'));
      console.log('');
    }
  }
}