jsdoc.js 9.24 KB
/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
"use strict";
function arrayIncludes(array, key) {
    for (var _i = 0, array_1 = array; _i < array_1.length; _i++) {
        var elem = array_1[_i];
        if (elem === key)
            return true;
    }
    return false;
}
/**
 * A list of all JSDoc tags allowed by the Closure compiler.
 * The public Closure docs don't list all the tags it allows; this list comes
 * from the compiler source itself.
 * https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/parsing/Annotation.java
 */
var JSDOC_TAGS_WHITELIST = [
    'ngInject',
    'abstract',
    'argument',
    'author',
    'consistentIdGenerator',
    'const',
    'constant',
    'constructor',
    'copyright',
    'define',
    'deprecated',
    'desc',
    'dict',
    'disposes',
    'enum',
    'export',
    'expose',
    'extends',
    'externs',
    'fileoverview',
    'final',
    'hidden',
    'idGenerator',
    'implements',
    'implicitCast',
    'inheritDoc',
    'interface',
    'record',
    'jaggerInject',
    'jaggerModule',
    'jaggerProvidePromise',
    'jaggerProvide',
    'lends',
    'license',
    'meaning',
    'modifies',
    'noalias',
    'nocollapse',
    'nocompile',
    'nosideeffects',
    'override',
    'owner',
    'package',
    'param',
    'polymerBehavior',
    'preserve',
    'preserveTry',
    'private',
    'protected',
    'public',
    'return',
    'returns',
    'see',
    'stableIdGenerator',
    'struct',
    'suppress',
    'template',
    'this',
    'throws',
    'type',
    'typedef',
    'unrestricted',
    'version',
    'wizaction',
];
/**
 * A list of JSDoc @tags that are never allowed in TypeScript source. These are Closure tags that
 * can be expressed in the TypeScript surface syntax. As tsickle's emit will mangle type names,
 * these will cause Closure Compiler issues and should not be used.
 */
var JSDOC_TAGS_BLACKLIST = [
    'constructor',
    'enum',
    'extends',
    'implements',
    'interface',
    'lends',
    'private',
    'public',
    'record',
    'template',
    'this',
    'type',
    'typedef',
];
/**
 * A list of JSDoc @tags that might include a {type} after them. Only banned when a type is passed.
 */
var JSDOC_TAGS_WITH_TYPES = [
    'const',
    'export',
    'param',
    'return',
];
/**
 * parse parses JSDoc out of a comment string.
 * Returns null if comment is not JSDoc.
 */
function parse(comment) {
    // TODO(evanm): this is a pile of hacky regexes for now, because we
    // would rather use the better TypeScript implementation of JSDoc
    // parsing.  https://github.com/Microsoft/TypeScript/issues/7393
    var match = comment.match(/^\/\*\*([\s\S]*?)\*\/$/);
    if (!match)
        return null;
    comment = match[1].trim();
    // Strip all the " * " bits from the front of each line.
    comment = comment.replace(/^\s*\*? ?/gm, '');
    var lines = comment.split('\n');
    var tags = [];
    var warnings = [];
    for (var _i = 0, lines_1 = lines; _i < lines_1.length; _i++) {
        var line = lines_1[_i];
        match = line.match(/^@(\S+) *(.*)/);
        if (match) {
            var _ = match[0], tagName = match[1], text = match[2];
            if (tagName === 'returns') {
                // A synonym for 'return'.
                tagName = 'return';
            }
            if (arrayIncludes(JSDOC_TAGS_BLACKLIST, tagName)) {
                warnings.push("@" + tagName + " annotations are redundant with TypeScript equivalents");
                continue; // Drop the tag so Closure won't process it.
            }
            else if (arrayIncludes(JSDOC_TAGS_WITH_TYPES, tagName) && text[0] === '{') {
                warnings.push("the type annotation on @" + tagName + " is redundant with its TypeScript type, " +
                    "remove the {...} part");
                continue;
            }
            else if (tagName === 'dict') {
                warnings.push('use index signatures (`[k: string]: type`) instead of @dict');
                continue;
            }
            // Grab the parameter name from @param tags.
            var parameterName = void 0;
            if (tagName === 'param') {
                match = text.match(/^(\S+) ?(.*)/);
                if (match)
                    _ = match[0], parameterName = match[1], text = match[2];
            }
            var tag = { tagName: tagName };
            if (parameterName)
                tag.parameterName = parameterName;
            if (text)
                tag.text = text;
            tags.push(tag);
        }
        else {
            // Text without a preceding @tag on it is either the plain text
            // documentation or a continuation of a previous tag.
            if (tags.length === 0) {
                tags.push({ text: line });
            }
            else {
                var lastTag = tags[tags.length - 1];
                lastTag.text = (lastTag.text || '') + '\n' + line;
            }
        }
    }
    if (warnings.length > 0) {
        return { tags: tags, warnings: warnings };
    }
    return { tags: tags };
}
exports.parse = parse;
/**
 * Serializes a Tag into a string usable in a comment.
 * Returns a string like " @foo {bar} baz" (note the whitespace).
 */
function tagToString(tag, escapeExtraTags) {
    if (escapeExtraTags === void 0) { escapeExtraTags = []; }
    var out = '';
    if (tag.tagName) {
        if (!arrayIncludes(JSDOC_TAGS_WHITELIST, tag.tagName) ||
            arrayIncludes(escapeExtraTags, tag.tagName)) {
            // Escape tags we don't understand.  This is a subtle
            // compromise between multiple issues.
            // 1) If we pass through these non-Closure tags, the user will
            //    get a warning from Closure, and the point of tsickle is
            //    to insulate the user from Closure.
            // 2) The output of tsickle is for Closure but also may be read
            //    by humans, for example non-TypeScript users of Angular.
            // 3) Finally, we don't want to warn because users should be
            //    free to add whichever JSDoc they feel like.  If the user
            //    wants help ensuring they didn't typo a tag, that is the
            //    responsibility of a linter.
            out += " \\@" + tag.tagName;
        }
        else {
            out += " @" + tag.tagName;
        }
    }
    if (tag.type) {
        out += ' {';
        if (tag.restParam) {
            out += '...';
        }
        out += tag.type;
        if (tag.optional) {
            out += '=';
        }
        out += '}';
    }
    if (tag.parameterName) {
        out += ' ' + tag.parameterName;
    }
    if (tag.text) {
        out += ' ' + tag.text.replace(/@/g, '\\@');
    }
    return out;
}
/** Serializes a Comment out to a string usable in source code. */
function toString(tags, escapeExtraTags) {
    if (escapeExtraTags === void 0) { escapeExtraTags = []; }
    if (tags.length === 0)
        return '';
    if (tags.length === 1) {
        var tag = tags[0];
        if (tag.tagName === 'type' && (!tag.text || !tag.text.match('\n'))) {
            // Special-case one-liner "type" tags to fit on one line, e.g.
            //   /** @type {foo} */
            return '/**' + tagToString(tag, escapeExtraTags) + ' */\n';
        }
    }
    var out = '';
    out += '/**\n';
    for (var _i = 0, tags_1 = tags; _i < tags_1.length; _i++) {
        var tag = tags_1[_i];
        out += ' *';
        // If the tagToString is multi-line, insert " * " prefixes on subsequent lines.
        out += tagToString(tag, escapeExtraTags).split('\n').join('\n * ');
        out += '\n';
    }
    out += ' */\n';
    return out;
}
exports.toString = toString;
/** Merges multiple tags (of the same tagName type) into a single unified tag. */
function merge(tags) {
    var tagNames = new Set();
    var parameterNames = new Set();
    var types = new Set();
    var texts = new Set();
    // If any of the tags are optional/rest, then the merged output is optional/rest.
    var optional = false;
    var restParam = false;
    for (var _i = 0, tags_2 = tags; _i < tags_2.length; _i++) {
        var tag_1 = tags_2[_i];
        if (tag_1.tagName)
            tagNames.add(tag_1.tagName);
        if (tag_1.parameterName)
            parameterNames.add(tag_1.parameterName);
        if (tag_1.type)
            types.add(tag_1.type);
        if (tag_1.text)
            texts.add(tag_1.text);
        if (tag_1.optional)
            optional = true;
        if (tag_1.restParam)
            restParam = true;
    }
    if (tagNames.size !== 1) {
        throw new Error("cannot merge differing tags: " + JSON.stringify(tags));
    }
    var tagName = tagNames.values().next().value;
    var parameterName = parameterNames.size > 0 ? Array.from(parameterNames).join('_or_') : undefined;
    var type = types.size > 0 ? Array.from(types).join('|') : undefined;
    var text = texts.size > 0 ? Array.from(texts).join(' / ') : undefined;
    var tag = { tagName: tagName, parameterName: parameterName, type: type, text: text };
    if (optional)
        tag.optional = true;
    if (restParam)
        tag.restParam = true;
    return tag;
}
exports.merge = merge;

//# sourceMappingURL=jsdoc.js.map