kitchensink
Version:
Dispatch's awesome components and style guide
556 lines (488 loc) • 15.3 kB
JavaScript
var assert = require('assert');
var commentParser = require('comment-parser');
var TypeParser = require('jsdoctypeparser').Parser;
var TypeBuilder = require('jsdoctypeparser').Builder;
// wtf but it needed to stop writing warnings to stdout
// and revert exceptions functionality
TypeBuilder.ENABLE_EXCEPTIONS = true;
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
var PARSERS = {
tag: commentParser.PARSERS.parse_tag,
type: commentParser.PARSERS.parse_type,
description: commentParser.PARSERS.parse_description,
};
// jscs:enable
var RE_SPACE = /\s/;
module.exports = {
/**
* @param {string} commentNode
* @returns {DocComment}
*/
createDocCommentByCommentNode: function(commentNode) {
var loc = commentNode.loc;
var lines = [Array(loc.start.column + 1).join(' '), '/*', commentNode.value, '*/']
.join('').split('\n').map(function(v) {
return v.substr(loc.start.column);
});
var value = lines.join('\n');
return new DocComment(value, loc);
},
doc: DocComment,
tag: DocTag,
type: DocType,
location: DocLocation
};
/**
* jsdoc comment object
*
* @param {string} value
* @param {{start: DocLocation}} loc
* @constructor
*/
function DocComment(value, loc) {
assert.equal(typeof value, 'string', '');
assert(typeof loc === 'object' && typeof loc.start === 'object' &&
typeof loc.start.line === 'number' && typeof loc.start.column === 'number' &&
loc.start.line > 0 && loc.start.column >= 0,
'Location object should contains start field with valid line and column numbers');
// parse comments
var _parsed = _parseComment(value) || {};
// fill up fields
this.loc = loc;
this.value = value;
this.lines = value.split('\n');
this.valid = _parsed.hasOwnProperty('line');
// doc parsed data
var _d = _parsed.source;
var _dog = _d && _d.indexOf('\n@');
this.description = !_d || _d[0] === '@' ?
null
: (_dog === -1 ? _d : _d.substr(0, _dog));
this.tags = (_parsed.tags || []).map(function(tag) {
return new DocTag(tag, new DocLocation(tag.line, 3, loc.start));
});
// calculate abstract flag
this.abstract = this.tags.some(function(v) {
return v.id === 'abstract' || v.id === 'virtual';
});
/**
* @param {function(this: DocComment, DocTag): DocComment} fn
* @returns {DocComment}
*/
this.iterate = function forEachTag(fn) {
this.tags.forEach(fn, this);
return this;
};
/**
* @param {string|Array.<string>} types
* @param {function(this: DocComment, DocTag): DocComment} fn
* @returns {DocComment}
*/
this.iterateByType = function iterateByTypes(types, fn) {
var k = 0;
types = Array.isArray(types) ? types : [types];
this.tags.forEach(function(tag) {
if (types.indexOf(tag.id) === -1) {
return;
}
fn.call(this, tag, k++);
}, this);
return this;
};
}
/**
* Simple jsdoc tag object
*
* @param {Object} tag object from comment parser, fields: tag, line, value, name, type, description
* @param {DocLocation} loc
* @constructor
*/
function DocTag(tag, loc) {
this.id = tag.tag;
this.line = tag.line;
this.value = tag.value;
this.name = undefined;
this.type = undefined;
this.description = tag.description;
this.optional = tag.optional;
this.default = tag.default;
this.loc = loc;
if (tag.name) {
this.name = {
loc: this.loc.shift(0, tag.value.indexOf(tag.name)),
value: tag.name
};
}
if (tag.type) {
this.type = new DocType(tag.type, this.loc.shift(0, tag.value.indexOf(tag.type)));
}
}
/**
* Parses jsdoctype string and provides several methods to work with it
*
* @param {string} type
* @param {DocLocation} loc
* @constructor
*/
function DocType(type, loc) {
assert(type, 'type can\'t be empty');
assert(loc, 'location should be passed');
this.value = type;
this.loc = loc;
var parsed = _parseDocType(type);
var data = parsed.valid ? _simplifyType(parsed) : [];
this.optional = parsed.optional;
this.variable = parsed.variable;
this.valid = parsed.valid;
if (parsed.valid && parsed.variable) {
this.loc.column = this.loc.column + 3; // ' * '.length
}
/**
* Match type
*
* @param {EsprimaNode} node
* @returns {boolean}
*/
this.match = function(node) {
return jsDocMatchType(data, node);
};
/**
* @param {function(TypeBuilder)} cb Calling for each node
* @returns {Array}
*/
this.iterate = function(cb) {
if (!parsed.valid) {
return [];
}
return _iterateDocTypes(parsed, cb);
};
}
/**
* DocLocation
*
* @constructor
* @param {number} line
* @param {number} column
* @param {(Object|DocLocation)} [rel]
*/
function DocLocation(line, column, rel) {
assert(Number.isFinite(line) && line >= 0, 'line should be greater than zero');
assert(Number.isFinite(column) && column >= 0, 'column should be greater than zero');
rel = rel || {};
this.line = Number(line) + Number(rel.line || 0);
this.column = Number(column) + Number(rel.column || 0);
}
/**
* Shift location by line and column
*
* @param {number|Object} line
* @param {number} [column]
* @returns {DocLocation}
*/
DocLocation.prototype.shift = function(line, column) {
if (typeof line === 'object') {
column = line.column;
line = line.line;
}
return new DocLocation(line, column, this);
};
/**
* Comment parsing helper
*
* @param {string} comment
* @returns {Object}
* @private
*/
function _parseComment(comment) {
return commentParser(comment, {
parsers: [
// parse tag
function(str) {
var res = PARSERS.tag.call(this, str);
res.data.value = str;
return res;
},
PARSERS.type,
_parseName,
PARSERS.description,
]
})[0];
}
/**
* analogue of str.match(/@(\S+)(?:\s+\{([^\}]+)\})?(?:\s+(\S+))?(?:\s+([^$]+))?/);
*
* @param {string} str raw jsdoc string
* @returns {?Object} parsed tag node
*/
function _parseName(str, data) {
if (data.errors && data.errors.length) {
return null;
}
var l = str.length;
var pos = _skipws(str);
var ch = '';
var name = '';
var brackets = 0;
var chevrons = 0;
var ticks = 0;
while (pos < l) {
ch = str[pos];
brackets += ch === '[' ? 1 : ch === ']' ? -1 : 0;
chevrons += ch === '<' ? 1 : ch === '>' ? -1 : 0;
if (ch === '`') {
ticks++;
}
if (ticks % 2 === 0 && chevrons === 0 && brackets === 0 && RE_SPACE.test(ch)) {
break;
}
name += ch;
pos ++;
}
if (brackets !== 0) { throw Error('Invalid `name`, unpaired brackets'); }
if (chevrons !== 0) { throw Error('Invalid `name`, unpaired chevrons'); }
if (ticks % 2 !== 0) { throw Error('Invalid `name`, unpaired ticks'); }
var res = {
name: name,
default: undefined,
optional: false,
required: false,
ticked: false
};
// strip ticks (support for ticked variables)
if (name[0] === '`' && name[name.length - 1] === '`') {
res.ticked = true;
name = name.slice(1, -1).trim();
}
// strip chevrons
if (name[0] === '<' && name[name.length - 1] === '>') {
res.required = true;
name = name.slice(1, -1).trim();
}
// strip brackets
if (name[0] === '[' && name[name.length - 1] === ']') {
res.optional = true;
name = name.slice(1, -1).trim();
var eqPos = name.indexOf('=');
if (eqPos !== -1) {
res.default = name.substr(eqPos + 1).trim().replace(/^(["'])(.+)(\1)$/, '$2');
name = name.substr(0, eqPos).trim();
}
}
res.name = name;
return {
source: str.substr(0, pos),
data: res
};
/**
* Returns the next to whitespace char position in a string
*
* @param {string} str
* @return {number}
*/
function _skipws(str) {
var i = 0;
var l = str.length;
var ch;
do {
ch = str[i];
if (ch !== ' ' && ch !== '\t') {
break;
}
} while (++i < l);
return i;
}
}
/**
* @param {string} typeString
* @returns {?Array.<SimplifiedType>} - parsed jsdoctype string as array
*/
function _parseDocType(typeString) {
var parser = new TypeParser();
var node;
try {
node = parser.parse(typeString);
node.valid = true;
} catch (e) {
node = {};
node.error = e.message;
node.valid = false;
}
return node;
}
/**
* @param {EsprimaNode} node
* @param {Function} cb
* @returns {Array}
*/
function _iterateDocTypes(node, cb) {
var res;
switch (true) {
case node instanceof TypeBuilder.TypeUnion:
// optional: boolean,
// nullable: boolean,
// variable: boolean,
// nonNullable: boolean,
// all: boolean,
// unknown: boolean,
// types: Array.<TypeName|GenericType|FunctionType|RecordType>
res = node.types.map(function(subnode) {
if (node.collectionNode) {
subnode.parentNode = node.collectionNode;
}
return _iterateDocTypes(subnode, cb);
}) || [];
if (node.nullable) {
var subnode = {typeName: 'null', collectionNode: node};
res.concat(cb(subnode));
}
break;
case node instanceof TypeBuilder.TypeName:
// name: string
node.typeName = node.name;
res = cb(node);
break;
case node instanceof TypeBuilder.GenericType:
// genericTypeName: string,
// parameterTypeUnions: Array.<TypeUnion>
node.typeName = node.genericTypeName.type;
res = cb(node) || [];
if (node.parameterTypeUnions) {
// node.parameterTypeUnions.collectionNode = node;
res.concat(node.parameterTypeUnions.map(function(subnode) {
subnode.parentNode = node;
_iterateDocTypes(subnode, cb);
}));
}
break;
case node instanceof TypeBuilder.FunctionType:
// parameterTypeUnions: Array.<TypeUnion>,
// returnTypeUnion: TypeUnion|null,
// isConstructor: boolean,
// contextTypeUnion: TypeUnion|null
node.typeName = 'Function';
res = cb(node) || [];
res.concat(node.parameterTypeUnions.map(function(subnode) {
subnode.parentNode = node;
_iterateDocTypes(subnode, cb);
}));
if (node.returnTypeUnion) {
node.returnTypeUnion.collectionNode = node;
res.concat(_iterateDocTypes(node.returnTypeUnion, cb));
}
if (node.contextTypeUnion) {
node.contextTypeUnion.collectionNode = node;
res.concat(_iterateDocTypes(node.contextTypeUnion, cb));
}
break;
case node instanceof TypeBuilder.RecordType:
// entries: Array.<RecordEntry>
node.typeName = 'Object';
res = cb(node) || [];
if (node.entries) {
res.concat(node.entries.map(function(subnode) {
subnode.parentNode = node;
_iterateDocTypes(subnode, cb);
}));
}
break;
case node instanceof TypeBuilder.RecordType.Entry:
// name: string,
// typeUnion: TypeUnion
node.typeUnion.collectionNode = node;
res = _iterateDocTypes(node.typeUnion, cb);
break;
case node instanceof TypeBuilder.ModuleName:
node.typeName = node.name;
node.module = true;
res = cb(node);
break;
default:
throw new Error('DocType: Unsupported doc node');
}
return res;
}
/**
* Converts AST jsDoc node to simple object
*
* @param {Object} node
* @returns {!Array.<SimplifiedType>}
* @see https://github.com/Kuniwak/jsdoctypeparser
*/
function _simplifyType(node) {
var res = [];
_iterateDocTypes(node, function(type) {
if (!type.parentNode && (!type.collectionNode || !type.collectionNode.parentNode)) {
res.push({type: type.typeName});
}
});
return res;
}
var jsPrimitives = 'string number boolean null undefined Object Function Array Date RegExp'
.toLowerCase().split(' ');
/**
* Compare parsed jsDocTypes with esprima node
*
* @param {SimplifiedType[]} variants - result of jsDocParseType
* @param {Object} argument - esprima source code node
*/
function jsDocMatchType(variants, argument) {
var i;
var l;
var variant;
var type;
var primitive;
var result = null;
for (i = 0, l = variants.length; i < l; i += 1) {
variant = variants[i][0] || variants[i];
if (variant.unknown || !variant.type) {
result = true;
break;
}
type = variant.type.toLowerCase();
primitive = jsPrimitives.indexOf(type) !== -1;
if (argument.type === 'Literal') {
if (argument.value === null) {
result = result || (type === 'null');
} else if (argument.value === undefined) {
result = result || (type === 'undefined');
} else if (typeof argument.value !== 'object') {
result = result || (type === typeof argument.value);
} else if (!argument.value.type) {
result = result || (type === (argument.value instanceof RegExp ? 'regexp' : 'object'));
} else {
result = result || (type === argument.value.type);
}
} else if (argument.type === 'ObjectExpression') {
result = result || (type === 'object');
result = result || (!primitive);
} else if (argument.type === 'ArrayExpression') {
result = result || (type === 'array');
} else if (argument.type === 'NewExpression' && type === 'object') {
result = true;
} else if (argument.type === 'NewExpression') {
var c = argument.callee;
var exam = c.name;
if (!exam && c.type === 'MemberExpression') {
if (c.object.type === 'ThisExpression') {
result = true;
break;
}
var cur = c;
exam = [];
while (cur.object) {
exam.unshift(cur.property.name);
cur = cur.object;
}
exam.unshift(cur.name);
exam = exam.join('.');
}
exam = exam.toLowerCase();
result = result || (type === exam);
}
if (result) {
break;
}
}
// variables, expressions, another behavior
return result !== false;
}