jsctags
Version:
jsctags generator
382 lines (298 loc) • 7.33 kB
JavaScript
const format = require('util').format;
const isString = require('lodash.isstring');
const includes = require('lodash.includes');
const isUndefined = require('lodash.isundefined');
const isObject = require('lodash.isplainobject');
const isArray = require('lodash.isarray');
const isFunction = require('lodash.isfunction');
const sortBy = require('lodash.sortby');
const without = require('lodash.without');
const get = require('lodash.get');
const objectHash = require('object-hash');
const clone = require('lodash.clonedeep');
const uuid = require('node-uuid');
const path = require('path');
const condense = require('./condenser');
const MATCHES = {
pos: /^(\d*?)\[\d*?\:\d*?\]-(\d*?)\[\d*?\:\d*?\]$/,
addr: /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
fn: /^fn\*?\(/,
args: /^fn\*?\((.*?)\)/,
arg: /^.*?\: (.*?)$/,
arrArg: /^\[/,
ret: /-> (.*?)$/,
lineno: /^\d*?\[(\d*?)\:\d*?\]-\d*?\[\d*?\:\d*?\]$/,
namespace: /\//
};
const DEFAULT_TYPES = [
/^\?/,
MATCHES.fn,
/^\</,
/^\{/,
/^\[/,
/^\+/,
'number',
'bool',
'string'
];
const TYPE_MAPPING = {
Number: 'number',
bool: 'boolean',
String: 'string',
RegExp: 'regexp'
};
const hashNode = function(n) {
const node = clone(n);
node.id = undefined;
node.namespace = undefined;
node.parent = undefined;
return objectHash(node);
};
const isDefaultType = function(type) {
// Nested object
if (isUndefined(type)) {
return true;
}
if (isFunction(type)) {
return true;
}
return DEFAULT_TYPES.some(dt => {
if (isString(dt)) {
return type === dt;
}
return dt.test(type);
});
};
const Parser = function(ctx) {
if (!(this instanceof Parser)) {
return new Parser(ctx);
}
const extname = path.extname(ctx.file);
const basename = path.basename(ctx.file, extname);
this.fileId = format('%s%s', basename, extname.replace(/\./, '`'));
this.ctx = ctx;
this.condense = ctx.condense || {};
this.tags = [];
this.define = {};
this.walked = [];
this.bySpan = {};
};
Parser.prototype.parse = function(fn) {
const self = this;
if (Object.keys(self.condense).length) {
return self.hasCondense(fn);
}
condense(self.ctx, (err, condense) => {
if (err) {
return fn(err);
}
self.condense = condense;
self.hasCondense(fn);
});
};
Parser.prototype.hasCondense = function(fn) {
if (this.condense['!define']) {
this.define = this.condense['!define'];
}
this.fromTree(this.condense);
this.clean();
this.tags = this.tags.sort((a, b) => {
return a.lineno - b.lineno;
});
fn(null, this.tags);
};
Parser.prototype.clean = function() {
if (!this.ctx.clean) {
return;
}
const onSpan = function(span) {
const tags = this.bySpan[span];
if (tags.length < 2) {
return;
}
this.tags = without(
this.tags,
sortBy(tags, tag => {
return (tag.namespace || '').split(/\./).length;
}).pop()
);
};
Object.keys(this.bySpan).forEach(onSpan, this);
};
Parser.prototype.fromTree = function(tree, parent) {
if (!isObject(tree)) {
return;
}
return Object.keys(tree)
.filter(key => {
return !/^!/.test(key);
})
.map(function(name) {
return this.onNode(name, tree[name], parent);
}, this);
};
Parser.prototype.namespace = function(node, parent) {
if (!parent) {
return;
}
if (parent.namespace) {
return format('%s.%s', parent.namespace, parent.name);
}
if (MATCHES.namespace.test(parent.name)) {
return;
}
if (parent.name === this.fileId) {
return;
}
return parent.name;
};
Parser.prototype.lineno = function(node) {
if (!isString(node['!span'])) {
return;
}
return Number(node['!span'].match(MATCHES.lineno).pop()) + 1;
};
Parser.prototype.returns = function(node) {
if (!isString(node['!type'])) {
return;
}
const ret = node['!type'].match(MATCHES.ret);
if (!ret || !Array.isArray(ret)) {
return 'void';
}
return this.type({
'!type': ret[1]
});
};
Parser.prototype.isFn = function(node) {
if (!isString(node['!type'])) {
return false;
}
return MATCHES.fn.test(node['!type']);
};
Parser.prototype.fnArgs = function(node) {
if (!isString(node['!type'])) {
return [];
}
const args = node['!type'].match(MATCHES.args);
if (!Array.isArray(args) || !args.length) {
return '';
}
return args
.pop()
.split(',')
.map(function(arg) {
const t = arg.match(MATCHES.arg);
if (!Array.isArray(t) || !t.length) {
return;
}
const type = t.pop();
if (this.define[type] && this.define[type]['!type']) {
return this.typeFn(this.define[type]['!type']);
}
if (type && MATCHES.arrArg.test(type)) {
return 'Array'.concat(type);
}
return type;
}, this)
.filter(type => {
return Boolean(type);
});
};
Parser.prototype.typeFn = function(node) {
const args = this.fnArgs(node);
const ret = this.returns(node);
return format('%s function(%s)', ret, args ? args.join(', ') : '');
};
Parser.prototype.type = function(node) {
if (!isString(node['!type'])) {
return;
}
if (this.isFn(node)) {
return this.typeFn(node);
}
const clean = node['!type'].replace(/^\+/, '');
const mapped = TYPE_MAPPING[clean];
if (mapped) {
return mapped;
}
return this.ctx.preserveType ? node['!type'] : clean;
};
Parser.prototype.kind = function(node) {
return this.isFn(node) ? 'f' : 'v';
};
Parser.prototype.addr = function(node) {
if (!isString(node['!span'])) {
return;
}
const pos = node['!span'].match(MATCHES.pos);
const end = pos.pop();
const start = pos.pop();
const blob = this.ctx.content.slice(start, end);
const regexp = blob
.split(/\n/)
.shift()
.replace(MATCHES.addr, '\\$&');
const str = new RegExp(regexp).toString();
return str;
};
Parser.prototype.walk = function(node, parent) {
const hash = parent ? hashNode(parent) : undefined;
const id = format('%s-%s', node, hash);
if (hash && includes(this.walked, id)) {
return;
}
if (parent) {
this.walked.push(id);
}
return get(this.condense, node, node);
};
Parser.prototype.push = function(tag) {
this.tags.push(tag);
const hasSpan = tag.origin && tag.origin['!span'];
if (!hasSpan) {
return;
}
const span = tag.origin['!span'];
if (!isArray(this.bySpan[span])) {
this.bySpan[span] = [];
}
this.bySpan[span].push(tag);
};
Parser.prototype.onNode = function(name, node, parent) {
if (!node) {
return false;
}
if (isString(node)) {
node = this.walk(node, parent);
}
if (!isObject(node)) {
return;
}
if (!isDefaultType(node['!type'])) {
return;
}
const tag = {
id: uuid.v1(),
name,
addr: this.addr(node),
kind: this.kind(node),
type: this.type(node),
lineno: this.lineno(node),
namespace: this.namespace(node, parent),
parent: parent ? parent.id : undefined,
origin: {
'!span': node['!span'],
'!type': node['!type'],
'!data': node['!data']
}
};
if (node['!type'] || node['!span']) {
this.push(tag);
}
this.fromTree(node, tag);
};
module.exports = function(ctx, fn) {
return new Parser(ctx).parse(fn);
};
module.exports.ctags = require('./ctags');