jsdoc2flow
Version:
Convert JSDoc comments into Flow/Typescript annotations
186 lines (156 loc) • 5.93 kB
JavaScript
const doctrine = require("doctrine")
const { parse } = require("comment-parser")
const _ = require("lodash")
// a function to add doctorine information into comment-parse tags
function injectCommentParserToDoctrine(tagDoctrine, tagCommentParser) {
const tag = tagDoctrine
// from comment-parser
tag.typeText = tagCommentParser.type
return tag
}
function injectDoctrineToCommentParser(tagDoctrine, tagCommentParser) {
const tag = tagCommentParser
tag.title = tagCommentParser.tag
// use type to store doctrine types and use typeText to store comment-parser type
tag.typeText = tagCommentParser.type
tag.type = tagDoctrine.type
// doctrine uses description
if (tagCommentParser.description === "") {
// example: /** @callback promiseMeCoroutine */
tag.description = tagDoctrine.description // tagCommentParser.name
}
return tag
}
function parseDoctrine(comment) {
try {
// doctrine doesn't support default values, so modify the comment
// value prior to feeding it to doctrine.
let commentValueDoctrine = comment.value
const paramRegExp = /(@param\s+{[^}]+}\s+)\[([^=])+=[^\]]+]/g
commentValueDoctrine = commentValueDoctrine.replace(paramRegExp, (match, p1, p2) => {
return `${p1}${p2}`
})
return doctrine.parse(commentValueDoctrine, { unwrap: true })
} catch (e) {
console.warn(e)
return { tags: [] }
}
}
/** Comments on ES exported functions/variables should be used for the actual declaration */
function addCommentToDeclaration(node) {
if (node.declaration && (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration")) {
if (node.leadingComments) {
node.declaration.leadingComments = node.leadingComments
} else if (node.trailingComments) {
node.declaration.trailingComments = node.trailingComments
} else if (node.comments) {
node.declaration.comments = node.comments
}
return true
}
return false
}
function parseCommentParser(comment) {
try {
// add jsdoc around the comment value so comment-parser can parse it correctly
const commentValueCommentParser = `/*${comment.value}*/`
return parse(commentValueCommentParser)[0]
} catch (e) {
console.warn(e)
return { tags: [] }
}
}
class Visitor {
constructor({ fixerIndex, sourceCode }) {
this.fixerIndex = fixerIndex
this.sourceCode = sourceCode
this.visitedComments = []
}
visit(node) {
if (addCommentToDeclaration(node)) {
return []
}
const newComments = []
const allComments = _.uniq(_.concat(node.leadingComments || [], node.comments || [], node.trailingComments || []))
// find a way to detect doc string that is related to commented code
// for (let iComment = 0; iComment < allComments.length; iComment++) {
// // if there is a line comment in between descard before it
// if (allComments[iComment].type === "Line") {
// allComments = allComments.slice(iComment + 1) // if iComment === len returns []
// // recursive
// }
// }
allComments.forEach((comment) => {
const found = this.visitedComments.find((visited) => _.isEqual(comment, visited))
if (!found) {
newComments.push(comment)
}
})
let fixes = []
for (const comment of newComments) {
// Doctrine
const resultDoctrine = parseDoctrine(comment)
const tagsDoctrine = resultDoctrine.tags
// Comment-Parser
const resultCommentParser = parseCommentParser(comment)
let tagsCommentParser
if (resultCommentParser) {
// normally
tagsCommentParser = resultCommentParser.tags
} else {
// find typeText manually
const maybeTypeText = comment.value.match(/@.*\s+{(.*)}/)
let typeText = ""
if (maybeTypeText && maybeTypeText.length >= 1) {
typeText = maybeTypeText[0]
}
tagsCommentParser = tagsDoctrine
for (let iTag = 0; iTag < tagsDoctrine.length; iTag++) {
tagsCommentParser[iTag] = injectCommentParserToDoctrine(tagsDoctrine[iTag], { type: typeText })
}
}
// final tags
const tags = tagsCommentParser
if (tagsCommentParser.length === tagsDoctrine.length) {
// merge comment parser info into doctorine
for (let iTag = 0; iTag < tagsDoctrine.length; iTag++) {
tags[iTag] = injectDoctrineToCommentParser(tagsDoctrine[iTag], tagsCommentParser[iTag])
}
} else {
// happens when comment parser supports something but doctorine does not
// merge comment parser info into doctorine
for (let iTag = 0; iTag < tagsCommentParser.length; iTag++) {
const tagCommentParser = tagsCommentParser[iTag]
// find the corresponding tag in doctrine
const tagDoctrine = tagsDoctrine.find(
(td) => td.title === tagCommentParser.tag && td.name === tagCommentParser.name
) || { type: tagCommentParser.type || tagCommentParser.name }
tags[iTag] = injectDoctrineToCommentParser(tagDoctrine, tagCommentParser)
}
}
let processedComment = false
for (const tag of tags) {
const fixer = this.fixerIndex.get(tag.title)
if (!fixer) {
continue
}
const context = {
comment,
code: this.sourceCode,
tags,
}
const newFixes = fixer.getFixes(tag, node, context)
fixes = _.concat(fixes, newFixes)
if (newFixes.length) {
processedComment = true
}
}
if (processedComment) {
this.visitedComments.push(comment)
}
}
return fixes
}
}
module.exports = Visitor