jsdoc-parser-extended
Version:
Extended JSDoc parser.
248 lines (201 loc) • 5.86 kB
JavaScript
const parseComment = require('./parseComment');
const assert = require('assert');
const _ = require('lodash');
const K = require('./constants');
const Tag = require('./Docstring__Tag');
const extractIdInfo = require('./Docstring__extractIdInfo');
const collectDescription = require('./Docstring__collectDescription');
const { CommentAnnotations } = require('./lintingRules')
/**
* An object representing a JSDoc comment (parsed using dox).
*
* @param {String} comment
* The JSDoc-compatible comment string to build from.
*/
function Docstring(comment, params) {
if (arguments.length === 1 && typeof comment === 'object') {
return Object.assign(this, comment);
}
const { nodeLocation, linter } = params
let commentNode;
try {
commentNode = parseComment(comment);
}
catch(e) {
linter.logRuleEntry({
rule: CommentAnnotations,
params: {
commentString: comment,
parseError: e
},
loc: linter.locationForNode(nodeLocation)
})
this.invalid = true
return this
}
if (commentNode.length === 0) {
linter.logRuleEntry({
rule: CommentAnnotations,
params: {
commentString: comment
},
loc: linter.locationForNode(nodeLocation)
})
this.invalid = true
return this
}
assert(commentNode.length === 1,
'Comment parser should yield a single node, not ' + commentNode.length + '! ' +
'Source:\n' + comment
);
this.loc = params.linter.locationForNodeWithOffset(params.nodeLocation, commentNode[0].line)
this.tags = commentNode[0].tags.map(function(tagNode) {
return new Tag(tagNode, params);
});
const typeDefInfo = extractTypeDefs(this.tags);
const tagsWithoutTypeDefs = typeDefInfo.tags;
const inferredData = prepareFromTags(commentNode[0], params, tagsWithoutTypeDefs);
Object.assign(this, inferredData, {
tags: tagsWithoutTypeDefs
});
this.typeDefs = typeDefInfo.typeDefs.map(typeDef => {
return Object.assign({}, typeDef, prepareFromTags({ description: '' }, params, typeDef.tags), {
name: typeDef.name,
namespace: inferredData.namespace
});
});
return this;
}
function prepareFromTags(commentNode, { nodeLocation, config }, tags) {
const info = {};
const idInfo = extractIdInfo(tags, { inferNamespaces: config.inferNamespaces });
info.name = idInfo.name;
info.namespace = idInfo.namespace;
info.description = collectDescription(commentNode, null, tags);
info.aliases = tags.filter(function(tag) {
return tag.type === 'alias';
}).reduce(function(map, tag) {
map[tag.typeInfo.name] = true;
return map;
}, {});
info.$location = (
(idInfo.namespace ? idInfo.namespace + K.NAMESPACE_SEP : '') +
(idInfo.name || '') +
' in ' +
nodeLocation
);
return info;
}
var Dpt = Docstring.prototype;
/**
* @return {Object} doc
*
* @return {String} doc.name
* If this is a module, it will be the name of the module found in a
* @module tag, or a @name tag. Otherwise, it's what may be found in a
* @name tag, a @property tag, or a @method tag. See [extractIdInfo]().
*
* @return {String} doc.namespace
* The namespace name found in a @namespace tag, if any.
*
* @return {String} doc.description
* The free-text found in the docstring.
*
* @return {Tag[]} doc.tags
* All the JSDoc tags found in the docstring.
*/
Docstring.prototype.toJSON = function() {
var docstring = _.pick(this, [
'name',
'namespace',
'description',
'loc',
]);
docstring.tags = this.tags.map(function(tag) {
return tag.toJSON();
});
return docstring;
};
Dpt.isInvalid = function() {
return this.invalid || this.tags.some(tag => tag.invalid)
}
Dpt.isModule = function() {
return this.hasTag('module') || this.hasTag('class');
};
Dpt.isInternal = function() {
return this.hasTag('internal') || this.hasTag('ignoredoc');
};
Dpt.doesLend = function() {
return this.hasTag('lends');
};
Dpt.getLentTo = function() {
return this.getTag('lends').typeInfo.name;
};
Dpt.hasMemberOf = function() {
return this.hasTag('memberOf');
};
Dpt.getExplicitReceiver = function() {
return this.getTag('memberOf').typeInfo.name;
};
Dpt.hasTag = function(type) {
return this.tags.some(findByType(type));
};
Dpt.getTag = function(type) {
return this.tags.filter(findByType(type))[0];
};
Dpt.hasAlias = function(alias) {
return alias in this.aliases;
};
Dpt.hasTypeOverride = function() {
return getTypeOverridingTags(this.tags).length > 0;
};
Dpt.markedAsExportedSymbol = function() {
return this.hasTag('export');
}
Dpt.getTypeOverride = function() {
var typedTags = getTypeOverridingTags(this.tags);
if (typedTags.filter(x => x.type !== 'property').length > 1) {
console.warn("%s: Document has multiple type overrides! %s",
this.$location,
JSON.stringify(typedTags.map(x => x && x.typeInfo && x.typeInfo))
);
}
return typedTags[0].typeInfo.type;
};
Dpt.overrideNamespace = function(namespace) {
this.namespace = namespace;
};
Dpt.addAlias = function(name) {
this.aliases[name] = true;
};
module.exports = Docstring;
function getTypeOverridingTags(tags) {
return tags.filter(function(tag) {
return K.TYPE_OVERRIDING_TAGS[tag.type] === true;
});
}
function findByType(type) {
return function(x) {
return x.type === type;
}
}
function extractTypeDefs(tags) {
return tags.reduce(function(state, tag) {
if (tag.type === 'callback' || tag.type === 'typedef') {
state.typeDefs.push({
name: tag.typeInfo.name,
tags: [ tag ],
});
}
else if (state.typeDefs.length) {
state.typeDefs[state.typeDefs.length-1].tags.push(tag);
}
else {
state.tags.push(tag);
}
return state;
}, {
tags: [],
typeDefs: [],
});
};