UNPKG

kitchensink

Version:

Dispatch's awesome components and style guide

294 lines (255 loc) 9.16 kB
var assert = require('assert'); var jsdoc = require('../jsdoc'); var esprimaHelpers = require('../esprima-helpers'); var validators = require('./validate-jsdoc/index'); /** @type {string[]} - list of function node types */ var functionNodeTypes = [ 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression', ]; /** @type {string[]} - list of node types with jsdocs */ var jsdocableTypes = [] .concat(functionNodeTypes); /** * Rule constructor * * @this {module:jscs/Rule} * @constructor */ module.exports = function() {}; module.exports.prototype = { /** * Load all rules and init them * * @param {Object} options * @throws {Error} If options is not an Object */ configure: function(options) { assert(typeof options === 'object', 'jsDoc option requires object value'); // rules structured by scopes-tags for jsdoc-tags var rulesForTags = this._rulesForTags = {}; // rules structured by scopes for nodes var rulesForNodes = this._rulesForNodes = {}; this._options = options; this._optionsList = Object.keys(options); // load validators this._validators = validators.load(this._optionsList); assert(this._validators.length, 'jsDoc plugin was not configured properly'); // registering validators this._validators.forEach(function(v) { // check options if (v.options) { validators.checkOptions(v, options); } // configure if (v.configure) { v.configure.call(this, options); } // index rules by tags and scopes (v.scopes || ['']).forEach(function(scope) { if (!v.tags) { assert(v.length === 2, 'jsDoc rules: Wrong arity in ' + v._name + ' validator'); rulesForNodes[scope] = rulesForNodes[scope] || []; rulesForNodes[scope].push(v); return; } assert(v.length === 3, 'jsDoc rules: Wrong arity in ' + v._name + ' validator'); rulesForTags[scope] = rulesForTags[scope] || {}; v.tags.forEach(function(tag) { var dtag = '@' + tag; rulesForTags[scope][dtag] = rulesForTags[scope][dtag] || []; rulesForTags[scope][dtag].push(v); }); }); }, this); }, /** * @returns {string} */ getOptionName: function() { return 'jsDoc'; }, /** * @param {module:jscs/JsFile} file * @param {module:jscs/Errors} errors */ check: function(file, errors) { patchNodesInFile(file); this._iterate = file.iterate; var _this = this; var scopes = { 'function': functionNodeTypes, }; // classic checker if (_this._rulesForNodes.file) { // call file checkers var validators = _this._rulesForNodes.file; if (validators) { validators.forEach(function(v) { v.call(_this, file, errors); }); } } // iterate over scopes Object.keys(scopes).forEach(function(scope) { // skip unused if (!_this._rulesForNodes[scope] && !_this._rulesForTags[scope]) { return; } // traverse ast tree and search scope node types file.iterateNodesByType(scopes[scope], function(node) { // init var commentStart = (node.jsdoc || node).loc.start; var commentStartLine = commentStart.line; var validators; // call node checkers validators = _this._rulesForNodes[scope]; if (validators) { validators.forEach(function(v) { v.call(_this, node, addError); }); } validators = _this._rulesForTags[scope]; if (!validators || !node.jsdoc || !node.jsdoc.valid) { return; } // call rule checkers node.jsdoc.iterate(function(tag, i) { if (!validators['@' + tag.id]) { return; } // call tag validator commentStart._line = commentStartLine + i; validators['@' + tag.id].forEach(function(v) { v.call(_this, node, tag, fixErrLocation(addError, tag)); }); }); /** * Send error to jscs * * @param {string} text * @param {number|DocLocation} relLine * @param {number} [relColumn] */ function addError(text, relLine, relColumn) { var line; var column; if (typeof relLine === 'object') { line = relLine.line; column = relLine.column; } else { line = commentStart._line + (relLine || 0); column = commentStart.column + (relColumn || 0); } errors.add(text, line, column); } /** * Generates function with location fixing logic to send error to jscs * * @param {function(string, number|Object, ?number)} err * @param {DocTag} tag * @returns {function(string, number|Object, ?number)} */ function fixErrLocation(err, tag) { return function(text, line, column) { line = line || tag.line; // probably buggy. multiline comment could resolved to 0 column = column || node.jsdoc.lines[tag.line].indexOf(tag.value); err(text, line, column); }; } }); }); }, /** * Caching scope search. todo: move to patchNodesInFile * * @param {Object} node */ _getReturnStatementsForNode: function(node) { if (node.jsdoc.returnStatements) { return node.jsdoc.returnStatements; } var statements = []; this._iterate(function(n) { if (n && n.type === 'ReturnStatement' && n.argument) { if (node === esprimaHelpers.closestScopeNode(n)) { statements.push(n.argument); } } }, node); node.jsdoc.returnStatements = statements; return statements; } }; /** * Extends each node with helper properties * * @param {Object} file */ function patchNodesInFile(file) { if (file._jsdocs) { return; } // jsdoc property for nodes file.iterateNodesByType(jsdocableTypes, function(node) { Object.defineProperty(node, 'jsdoc', { get: getJsdoc }); }); /** * Fetchs jsdoc block for this * * @this {module:esprima/Node} * @returns {DocComment} */ function getJsdoc() { if (this.hasOwnProperty('_jsdoc')) { return this._jsdoc; } var node = functionNodeTypes.indexOf(this.type) !== -1 ? findFirstNodeInLine(this) : this; var res = findDocCommentBeforeNode(node, this); this._jsdoc = res ? jsdoc.createDocCommentByCommentNode(res) : null; return this._jsdoc; } /** * Finds the first node on the same line as passed node * * @param {?module:esprima/Node} node * @returns {?module:esprima/Node} */ function findFirstNodeInLine(node) { var parent = node.parentNode; if (!parent || parent.loc.start.line !== node.loc.start.line) { return node; } return findFirstNodeInLine(parent); } /** * Finds DocComment in file before passed line number * * @param {?module:esprima/Node} node * @returns {?module:esprima/Node} */ function findDocCommentBeforeNode(node, self) { var res = file.getPrevToken(file.getFirstNodeToken(node), {includeComments: true}); if (!res || res.type !== 'Block' || res.value.charAt(0) !== '*') { return null; } // Indent should be the same if (res.loc.start.column !== node.loc.start.column) { return null; } // IIFE should be on the next line to be sticked if ((self.type === 'FunctionExpression' || self.type === 'ArrowFunctionExpression') && self.parentNode.type === 'CallExpression' && (self.loc.start.line > res.loc.end.line + 2)) { return null; } return res; } // mark object as patched file._jsdocs = true; }