@microsoft/tsdoc
Version:
A parser for the TypeScript doc comment syntax
842 lines • 94.6 kB
JavaScript
"use strict";
/* eslint-disable max-lines */
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeParser = void 0;
var Token_1 = require("./Token");
var Tokenizer_1 = require("./Tokenizer");
var nodes_1 = require("../nodes");
var TokenSequence_1 = require("./TokenSequence");
var TokenReader_1 = require("./TokenReader");
var StringChecks_1 = require("./StringChecks");
var TSDocTagDefinition_1 = require("../configuration/TSDocTagDefinition");
var StandardTags_1 = require("../details/StandardTags");
var PlainTextEmitter_1 = require("../emitters/PlainTextEmitter");
var TSDocMessageId_1 = require("./TSDocMessageId");
function isFailure(resultOrFailure) {
return resultOrFailure !== undefined && Object.hasOwnProperty.call(resultOrFailure, 'failureMessage');
}
/**
* The main parser for TSDoc comments.
*/
var NodeParser = /** @class */ (function () {
function NodeParser(parserContext) {
this._parserContext = parserContext;
this._configuration = parserContext.configuration;
this._currentSection = parserContext.docComment.summarySection;
}
NodeParser.prototype.parse = function () {
var tokenReader = new TokenReader_1.TokenReader(this._parserContext);
var done = false;
while (!done) {
// Extract the next token
switch (tokenReader.peekTokenKind()) {
case Token_1.TokenKind.EndOfInput:
done = true;
break;
case Token_1.TokenKind.Newline:
this._pushAccumulatedPlainText(tokenReader);
tokenReader.readToken();
this._pushNode(new nodes_1.DocSoftBreak({
parsed: true,
configuration: this._configuration,
softBreakExcerpt: tokenReader.extractAccumulatedSequence()
}));
break;
case Token_1.TokenKind.Backslash:
this._pushAccumulatedPlainText(tokenReader);
this._pushNode(this._parseBackslashEscape(tokenReader));
break;
case Token_1.TokenKind.AtSign:
this._pushAccumulatedPlainText(tokenReader);
this._parseAndPushBlock(tokenReader);
break;
case Token_1.TokenKind.LeftCurlyBracket: {
this._pushAccumulatedPlainText(tokenReader);
var marker = tokenReader.createMarker();
var docNode = this._parseInlineTag(tokenReader);
var docComment = this._parserContext.docComment;
if (docNode instanceof nodes_1.DocInheritDocTag) {
// The @inheritDoc tag is irregular because it looks like an inline tag, but
// it actually represents the entire comment body
var tagEndMarker = tokenReader.createMarker() - 1;
if (docComment.inheritDocTag === undefined) {
this._parserContext.docComment.inheritDocTag = docNode;
}
else {
this._pushNode(this._backtrackAndCreateErrorRange(tokenReader, marker, tagEndMarker, TSDocMessageId_1.TSDocMessageId.ExtraInheritDocTag, 'A doc comment cannot have more than one @inheritDoc tag'));
}
}
else {
this._pushNode(docNode);
}
break;
}
case Token_1.TokenKind.RightCurlyBracket:
this._pushAccumulatedPlainText(tokenReader);
this._pushNode(this._createError(tokenReader, TSDocMessageId_1.TSDocMessageId.EscapeRightBrace, 'The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag'));
break;
case Token_1.TokenKind.LessThan:
this._pushAccumulatedPlainText(tokenReader);
// Look ahead two tokens to see if this is "<a>" or "</a>".
if (tokenReader.peekTokenAfterKind() === Token_1.TokenKind.Slash) {
this._pushNode(this._parseHtmlEndTag(tokenReader));
}
else {
this._pushNode(this._parseHtmlStartTag(tokenReader));
}
break;
case Token_1.TokenKind.GreaterThan:
this._pushAccumulatedPlainText(tokenReader);
this._pushNode(this._createError(tokenReader, TSDocMessageId_1.TSDocMessageId.EscapeGreaterThan, 'The ">" character should be escaped using a backslash to avoid confusion with an HTML tag'));
break;
case Token_1.TokenKind.Backtick:
this._pushAccumulatedPlainText(tokenReader);
if (tokenReader.peekTokenAfterKind() === Token_1.TokenKind.Backtick &&
tokenReader.peekTokenAfterAfterKind() === Token_1.TokenKind.Backtick) {
this._pushNode(this._parseFencedCode(tokenReader));
}
else {
this._pushNode(this._parseCodeSpan(tokenReader));
}
break;
default:
// If nobody recognized this token, then accumulate plain text
tokenReader.readToken();
break;
}
}
this._pushAccumulatedPlainText(tokenReader);
this._performValidationChecks();
};
NodeParser.prototype._performValidationChecks = function () {
var docComment = this._parserContext.docComment;
if (docComment.deprecatedBlock) {
if (!PlainTextEmitter_1.PlainTextEmitter.hasAnyTextContent(docComment.deprecatedBlock)) {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.MissingDeprecationMessage, "The " + docComment.deprecatedBlock.blockTag.tagName + " block must include a deprecation message," +
" e.g. describing the recommended alternative", docComment.deprecatedBlock.blockTag.getTokenSequence(), docComment.deprecatedBlock);
}
}
if (docComment.inheritDocTag) {
if (docComment.remarksBlock) {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.InheritDocIncompatibleTag, "A \"" + docComment.remarksBlock.blockTag.tagName + "\" block must not be used, because that" +
" content is provided by the @inheritDoc tag", docComment.remarksBlock.blockTag.getTokenSequence(), docComment.remarksBlock.blockTag);
}
if (PlainTextEmitter_1.PlainTextEmitter.hasAnyTextContent(docComment.summarySection)) {
this._parserContext.log.addMessageForTextRange(TSDocMessageId_1.TSDocMessageId.InheritDocIncompatibleSummary, 'The summary section must not have any content, because that' +
' content is provided by the @inheritDoc tag', this._parserContext.commentRange);
}
}
};
NodeParser.prototype._validateTagDefinition = function (tagDefinition, tagName, expectingInlineTag, tokenSequenceForErrorContext, nodeForErrorContext) {
if (tagDefinition) {
var isInlineTag = tagDefinition.syntaxKind === TSDocTagDefinition_1.TSDocTagSyntaxKind.InlineTag;
if (isInlineTag !== expectingInlineTag) {
// The tag is defined, but it is used incorrectly
if (expectingInlineTag) {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.TagShouldNotHaveBraces, "The TSDoc tag \"" + tagName + "\" is not an inline tag; it must not be enclosed in \"{ }\" braces", tokenSequenceForErrorContext, nodeForErrorContext);
}
else {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.InlineTagMissingBraces, "The TSDoc tag \"" + tagName + "\" is an inline tag; it must be enclosed in \"{ }\" braces", tokenSequenceForErrorContext, nodeForErrorContext);
}
}
else {
if (this._parserContext.configuration.validation.reportUnsupportedTags) {
if (!this._parserContext.configuration.isTagSupported(tagDefinition)) {
// The tag is defined, but not supported
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.UnsupportedTag, "The TSDoc tag \"" + tagName + "\" is not supported by this tool", tokenSequenceForErrorContext, nodeForErrorContext);
}
}
}
}
else {
// The tag is not defined
if (!this._parserContext.configuration.validation.ignoreUndefinedTags) {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.UndefinedTag, "The TSDoc tag \"" + tagName + "\" is not defined in this configuration", tokenSequenceForErrorContext, nodeForErrorContext);
}
}
};
NodeParser.prototype._pushAccumulatedPlainText = function (tokenReader) {
if (!tokenReader.isAccumulatedSequenceEmpty()) {
this._pushNode(new nodes_1.DocPlainText({
parsed: true,
configuration: this._configuration,
textExcerpt: tokenReader.extractAccumulatedSequence()
}));
}
};
NodeParser.prototype._parseAndPushBlock = function (tokenReader) {
var docComment = this._parserContext.docComment;
var configuration = this._parserContext.configuration;
var modifierTagSet = docComment.modifierTagSet;
var parsedBlockTag = this._parseBlockTag(tokenReader);
if (parsedBlockTag.kind !== nodes_1.DocNodeKind.BlockTag) {
this._pushNode(parsedBlockTag);
return;
}
var docBlockTag = parsedBlockTag;
// Do we have a definition for this tag?
var tagDefinition = configuration.tryGetTagDefinitionWithUpperCase(docBlockTag.tagNameWithUpperCase);
this._validateTagDefinition(tagDefinition, docBlockTag.tagName,
/* expectingInlineTag */ false, docBlockTag.getTokenSequence(), docBlockTag);
if (tagDefinition) {
switch (tagDefinition.syntaxKind) {
case TSDocTagDefinition_1.TSDocTagSyntaxKind.BlockTag:
if (docBlockTag.tagNameWithUpperCase === StandardTags_1.StandardTags.param.tagNameWithUpperCase) {
var docParamBlock = this._parseParamBlock(tokenReader, docBlockTag, StandardTags_1.StandardTags.param.tagName);
this._parserContext.docComment.params.add(docParamBlock);
this._currentSection = docParamBlock.content;
return;
}
else if (docBlockTag.tagNameWithUpperCase === StandardTags_1.StandardTags.typeParam.tagNameWithUpperCase) {
var docParamBlock = this._parseParamBlock(tokenReader, docBlockTag, StandardTags_1.StandardTags.typeParam.tagName);
this._parserContext.docComment.typeParams.add(docParamBlock);
this._currentSection = docParamBlock.content;
return;
}
else {
var newBlock = new nodes_1.DocBlock({
configuration: this._configuration,
blockTag: docBlockTag
});
this._addBlockToDocComment(newBlock);
this._currentSection = newBlock.content;
}
return;
case TSDocTagDefinition_1.TSDocTagSyntaxKind.ModifierTag:
// The block tag was recognized as a modifier, so add it to the modifier tag set
// and do NOT call currentSection.appendNode(parsedNode)
modifierTagSet.addTag(docBlockTag);
return;
}
}
this._pushNode(docBlockTag);
};
NodeParser.prototype._addBlockToDocComment = function (block) {
var docComment = this._parserContext.docComment;
switch (block.blockTag.tagNameWithUpperCase) {
case StandardTags_1.StandardTags.remarks.tagNameWithUpperCase:
docComment.remarksBlock = block;
break;
case StandardTags_1.StandardTags.privateRemarks.tagNameWithUpperCase:
docComment.privateRemarks = block;
break;
case StandardTags_1.StandardTags.deprecated.tagNameWithUpperCase:
docComment.deprecatedBlock = block;
break;
case StandardTags_1.StandardTags.returns.tagNameWithUpperCase:
docComment.returnsBlock = block;
break;
case StandardTags_1.StandardTags.see.tagNameWithUpperCase:
docComment._appendSeeBlock(block);
break;
default:
docComment.appendCustomBlock(block);
}
};
/**
* Used by `_parseParamBlock()`, this parses a JSDoc expression remainder like `string}` or `="]"]` from
* an input like `@param {string} [x="]"] - the X value`. It detects nested balanced pairs of delimiters
* and escaped string literals.
*/
NodeParser.prototype._tryParseJSDocTypeOrValueRest = function (tokenReader, openKind, closeKind, startMarker) {
var quoteKind;
var openCount = 1;
while (openCount > 0) {
var tokenKind = tokenReader.peekTokenKind();
switch (tokenKind) {
case openKind:
// ignore open bracket/brace inside of a quoted string
if (quoteKind === undefined)
openCount++;
break;
case closeKind:
// ignore close bracket/brace inside of a quoted string
if (quoteKind === undefined)
openCount--;
break;
case Token_1.TokenKind.Backslash:
// ignore backslash outside of quoted string
if (quoteKind !== undefined) {
// skip the backslash and the next character.
tokenReader.readToken();
tokenKind = tokenReader.peekTokenKind();
}
break;
case Token_1.TokenKind.DoubleQuote:
case Token_1.TokenKind.SingleQuote:
case Token_1.TokenKind.Backtick:
if (quoteKind === tokenKind) {
// exit quoted string if quote character matches.
quoteKind = undefined;
}
else if (quoteKind === undefined) {
// start quoted string if not in a quoted string.
quoteKind = tokenKind;
}
break;
}
// give up at end of input and backtrack to start.
if (tokenKind === Token_1.TokenKind.EndOfInput) {
tokenReader.backtrackToMarker(startMarker);
return undefined;
}
tokenReader.readToken();
}
return tokenReader.tryExtractAccumulatedSequence();
};
/**
* Used by `_parseParamBlock()`, this parses a JSDoc expression like `{string}` from
* an input like `@param {string} x - the X value`.
*/
NodeParser.prototype._tryParseUnsupportedJSDocType = function (tokenReader, docBlockTag, tagName) {
tokenReader.assertAccumulatedSequenceIsEmpty();
// do not parse `{@...` as a JSDoc type
if (tokenReader.peekTokenKind() !== Token_1.TokenKind.LeftCurlyBracket ||
tokenReader.peekTokenAfterKind() === Token_1.TokenKind.AtSign) {
return undefined;
}
var startMarker = tokenReader.createMarker();
tokenReader.readToken(); // read the "{"
var jsdocTypeExcerpt = this._tryParseJSDocTypeOrValueRest(tokenReader, Token_1.TokenKind.LeftCurlyBracket, Token_1.TokenKind.RightCurlyBracket, startMarker);
if (jsdocTypeExcerpt) {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.ParamTagWithInvalidType, 'The ' + tagName + " block should not include a JSDoc-style '{type}'", jsdocTypeExcerpt, docBlockTag);
var spacingAfterJsdocTypeExcerpt = this._tryReadSpacingAndNewlines(tokenReader);
if (spacingAfterJsdocTypeExcerpt) {
jsdocTypeExcerpt = jsdocTypeExcerpt.getNewSequence(jsdocTypeExcerpt.startIndex, spacingAfterJsdocTypeExcerpt.endIndex);
}
}
return jsdocTypeExcerpt;
};
/**
* Used by `_parseParamBlock()`, this parses a JSDoc expression remainder like `=[]]` from
* an input like `@param {string} [x=[]] - the X value`.
*/
NodeParser.prototype._tryParseJSDocOptionalNameRest = function (tokenReader) {
tokenReader.assertAccumulatedSequenceIsEmpty();
if (tokenReader.peekTokenKind() !== Token_1.TokenKind.EndOfInput) {
var startMarker = tokenReader.createMarker();
return this._tryParseJSDocTypeOrValueRest(tokenReader, Token_1.TokenKind.LeftSquareBracket, Token_1.TokenKind.RightSquareBracket, startMarker);
}
return undefined;
};
NodeParser.prototype._parseParamBlock = function (tokenReader, docBlockTag, tagName) {
var startMarker = tokenReader.createMarker();
var spacingBeforeParameterNameExcerpt = this._tryReadSpacingAndNewlines(tokenReader);
// Skip past a JSDoc type (i.e., '@param {type} paramName') if found, and report a warning.
var unsupportedJsdocTypeBeforeParameterNameExcerpt = this._tryParseUnsupportedJSDocType(tokenReader, docBlockTag, tagName);
// Parse opening of invalid JSDoc optional parameter name (e.g., '[')
var unsupportedJsdocOptionalNameOpenBracketExcerpt;
if (tokenReader.peekTokenKind() === Token_1.TokenKind.LeftSquareBracket) {
tokenReader.readToken(); // read the "["
unsupportedJsdocOptionalNameOpenBracketExcerpt = tokenReader.extractAccumulatedSequence();
}
var parameterName = '';
var done = false;
while (!done) {
switch (tokenReader.peekTokenKind()) {
case Token_1.TokenKind.AsciiWord:
case Token_1.TokenKind.Period:
case Token_1.TokenKind.DollarSign:
parameterName += tokenReader.readToken();
break;
default:
done = true;
break;
}
}
var explanation = StringChecks_1.StringChecks.explainIfInvalidUnquotedIdentifier(parameterName);
if (explanation !== undefined) {
tokenReader.backtrackToMarker(startMarker);
var errorParamBlock = new nodes_1.DocParamBlock({
configuration: this._configuration,
blockTag: docBlockTag,
parameterName: ''
});
var errorMessage = parameterName.length > 0
? 'The ' + tagName + ' block should be followed by a valid parameter name: ' + explanation
: 'The ' + tagName + ' block should be followed by a parameter name';
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.ParamTagWithInvalidName, errorMessage, docBlockTag.getTokenSequence(), docBlockTag);
return errorParamBlock;
}
var parameterNameExcerpt = tokenReader.extractAccumulatedSequence();
// Parse closing of invalid JSDoc optional parameter name (e.g., ']', '=default]').
var unsupportedJsdocOptionalNameRestExcerpt;
if (unsupportedJsdocOptionalNameOpenBracketExcerpt) {
unsupportedJsdocOptionalNameRestExcerpt = this._tryParseJSDocOptionalNameRest(tokenReader);
var errorSequence = unsupportedJsdocOptionalNameOpenBracketExcerpt;
if (unsupportedJsdocOptionalNameRestExcerpt) {
errorSequence = docBlockTag
.getTokenSequence()
.getNewSequence(unsupportedJsdocOptionalNameOpenBracketExcerpt.startIndex, unsupportedJsdocOptionalNameRestExcerpt.endIndex);
}
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.ParamTagWithInvalidOptionalName, 'The ' +
tagName +
" should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets.", errorSequence, docBlockTag);
}
var spacingAfterParameterNameExcerpt = this._tryReadSpacingAndNewlines(tokenReader);
// Skip past a trailing JSDoc type (i.e., '@param paramName {type}') if found, and report a warning.
var unsupportedJsdocTypeAfterParameterNameExcerpt = this._tryParseUnsupportedJSDocType(tokenReader, docBlockTag, tagName);
// TODO: Warn if there is no space before or after the hyphen
var hyphenExcerpt;
var spacingAfterHyphenExcerpt;
var unsupportedJsdocTypeAfterHyphenExcerpt;
if (tokenReader.peekTokenKind() === Token_1.TokenKind.Hyphen) {
tokenReader.readToken();
hyphenExcerpt = tokenReader.extractAccumulatedSequence();
// TODO: Only read one space
spacingAfterHyphenExcerpt = this._tryReadSpacingAndNewlines(tokenReader);
// Skip past a JSDoc type (i.e., '@param paramName - {type}') if found, and report a warning.
unsupportedJsdocTypeAfterHyphenExcerpt = this._tryParseUnsupportedJSDocType(tokenReader, docBlockTag, tagName);
}
else {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.ParamTagMissingHyphen, 'The ' + tagName + ' block should be followed by a parameter name and then a hyphen', docBlockTag.getTokenSequence(), docBlockTag);
}
return new nodes_1.DocParamBlock({
parsed: true,
configuration: this._configuration,
blockTag: docBlockTag,
spacingBeforeParameterNameExcerpt: spacingBeforeParameterNameExcerpt,
unsupportedJsdocTypeBeforeParameterNameExcerpt: unsupportedJsdocTypeBeforeParameterNameExcerpt,
unsupportedJsdocOptionalNameOpenBracketExcerpt: unsupportedJsdocOptionalNameOpenBracketExcerpt,
parameterNameExcerpt: parameterNameExcerpt,
parameterName: parameterName,
unsupportedJsdocOptionalNameRestExcerpt: unsupportedJsdocOptionalNameRestExcerpt,
spacingAfterParameterNameExcerpt: spacingAfterParameterNameExcerpt,
unsupportedJsdocTypeAfterParameterNameExcerpt: unsupportedJsdocTypeAfterParameterNameExcerpt,
hyphenExcerpt: hyphenExcerpt,
spacingAfterHyphenExcerpt: spacingAfterHyphenExcerpt,
unsupportedJsdocTypeAfterHyphenExcerpt: unsupportedJsdocTypeAfterHyphenExcerpt
});
};
NodeParser.prototype._pushNode = function (docNode) {
if (this._configuration.docNodeManager.isAllowedChild(nodes_1.DocNodeKind.Paragraph, docNode.kind)) {
this._currentSection.appendNodeInParagraph(docNode);
}
else {
this._currentSection.appendNode(docNode);
}
};
NodeParser.prototype._parseBackslashEscape = function (tokenReader) {
tokenReader.assertAccumulatedSequenceIsEmpty();
var marker = tokenReader.createMarker();
tokenReader.readToken(); // read the backslash
if (tokenReader.peekTokenKind() === Token_1.TokenKind.EndOfInput) {
return this._backtrackAndCreateError(tokenReader, marker, TSDocMessageId_1.TSDocMessageId.UnnecessaryBackslash, 'A backslash must precede another character that is being escaped');
}
var escapedToken = tokenReader.readToken(); // read the escaped character
// In CommonMark, a backslash is only allowed before a punctuation
// character. In all other contexts, the backslash is interpreted as a
// literal character.
if (!Tokenizer_1.Tokenizer.isPunctuation(escapedToken.kind)) {
return this._backtrackAndCreateError(tokenReader, marker, TSDocMessageId_1.TSDocMessageId.UnnecessaryBackslash, 'A backslash can only be used to escape a punctuation character');
}
var encodedTextExcerpt = tokenReader.extractAccumulatedSequence();
return new nodes_1.DocEscapedText({
parsed: true,
configuration: this._configuration,
escapeStyle: nodes_1.EscapeStyle.CommonMarkBackslash,
encodedTextExcerpt: encodedTextExcerpt,
decodedText: escapedToken.toString()
});
};
NodeParser.prototype._parseBlockTag = function (tokenReader) {
tokenReader.assertAccumulatedSequenceIsEmpty();
var marker = tokenReader.createMarker();
if (tokenReader.peekTokenKind() !== Token_1.TokenKind.AtSign) {
return this._backtrackAndCreateError(tokenReader, marker, TSDocMessageId_1.TSDocMessageId.MissingTag, 'Expecting a TSDoc tag starting with "@"');
}
// "@one" is a valid TSDoc tag at the start of a line, but "@one@two" is
// a syntax error. For two tags it should be "@one @two", or for literal text it
// should be "\@one\@two".
switch (tokenReader.peekPreviousTokenKind()) {
case Token_1.TokenKind.EndOfInput:
case Token_1.TokenKind.Spacing:
case Token_1.TokenKind.Newline:
break;
default:
return this._backtrackAndCreateError(tokenReader, marker, TSDocMessageId_1.TSDocMessageId.AtSignInWord, 'The "@" character looks like part of a TSDoc tag; use a backslash to escape it');
}
// Include the "@" as part of the tagName
var tagName = tokenReader.readToken().toString();
if (tokenReader.peekTokenKind() !== Token_1.TokenKind.AsciiWord) {
return this._backtrackAndCreateError(tokenReader, marker, TSDocMessageId_1.TSDocMessageId.AtSignWithoutTagName, 'Expecting a TSDoc tag name after "@"; if it is not a tag, use a backslash to escape this character');
}
var tagNameMarker = tokenReader.createMarker();
while (tokenReader.peekTokenKind() === Token_1.TokenKind.AsciiWord) {
tagName += tokenReader.readToken().toString();
}
switch (tokenReader.peekTokenKind()) {
case Token_1.TokenKind.Spacing:
case Token_1.TokenKind.Newline:
case Token_1.TokenKind.EndOfInput:
break;
default:
var badCharacter = tokenReader.peekToken().range.toString()[0];
return this._backtrackAndCreateError(tokenReader, marker, TSDocMessageId_1.TSDocMessageId.CharactersAfterBlockTag, "The token \"" + tagName + "\" looks like a TSDoc tag but contains an invalid character" +
(" " + JSON.stringify(badCharacter) + "; if it is not a tag, use a backslash to escape the \"@\""));
}
if (StringChecks_1.StringChecks.explainIfInvalidTSDocTagName(tagName)) {
var failure = this._createFailureForTokensSince(tokenReader, TSDocMessageId_1.TSDocMessageId.MalformedTagName, 'A TSDoc tag name must start with a letter and contain only letters and numbers', tagNameMarker);
return this._backtrackAndCreateErrorForFailure(tokenReader, marker, '', failure);
}
return new nodes_1.DocBlockTag({
parsed: true,
configuration: this._configuration,
tagName: tagName,
tagNameExcerpt: tokenReader.extractAccumulatedSequence()
});
};
NodeParser.prototype._parseInlineTag = function (tokenReader) {
tokenReader.assertAccumulatedSequenceIsEmpty();
var marker = tokenReader.createMarker();
if (tokenReader.peekTokenKind() !== Token_1.TokenKind.LeftCurlyBracket) {
return this._backtrackAndCreateError(tokenReader, marker, TSDocMessageId_1.TSDocMessageId.MissingTag, 'Expecting a TSDoc tag starting with "{"');
}
tokenReader.readToken();
var openingDelimiterExcerpt = tokenReader.extractAccumulatedSequence();
// For inline tags, if we handle errors by backtracking to the "{" token, then the main loop
// will then interpret the "@" as a block tag, which is almost certainly incorrect. So the
// DocErrorText needs to include both the "{" and "@" tokens.
// We will use _backtrackAndCreateErrorRangeForFailure() for that.
var atSignMarker = tokenReader.createMarker();
if (tokenReader.peekTokenKind() !== Token_1.TokenKind.AtSign) {
return this._backtrackAndCreateError(tokenReader, marker, TSDocMessageId_1.TSDocMessageId.MalformedInlineTag, 'Expecting a TSDoc tag starting with "{@"');
}
// Include the "@" as part of the tagName
var tagName = tokenReader.readToken().toString();
while (tokenReader.peekTokenKind() === Token_1.TokenKind.AsciiWord) {
tagName += tokenReader.readToken().toString();
}
if (tagName === '@') {
// This is an unusual case
var failure = this._createFailureForTokensSince(tokenReader, TSDocMessageId_1.TSDocMessageId.MalformedInlineTag, 'Expecting a TSDoc inline tag name after the "{@" characters', atSignMarker);
return this._backtrackAndCreateErrorRangeForFailure(tokenReader, marker, atSignMarker, '', failure);
}
if (StringChecks_1.StringChecks.explainIfInvalidTSDocTagName(tagName)) {
var failure = this._createFailureForTokensSince(tokenReader, TSDocMessageId_1.TSDocMessageId.MalformedTagName, 'A TSDoc tag name must start with a letter and contain only letters and numbers', atSignMarker);
return this._backtrackAndCreateErrorRangeForFailure(tokenReader, marker, atSignMarker, '', failure);
}
var tagNameExcerpt = tokenReader.extractAccumulatedSequence();
var spacingAfterTagNameExcerpt = this._tryReadSpacingAndNewlines(tokenReader);
if (spacingAfterTagNameExcerpt === undefined) {
// If there were no spaces at all, that's an error unless it's the degenerate "{@tag}" case
if (tokenReader.peekTokenKind() !== Token_1.TokenKind.RightCurlyBracket) {
var badCharacter = tokenReader.peekToken().range.toString()[0];
var failure = this._createFailureForToken(tokenReader, TSDocMessageId_1.TSDocMessageId.CharactersAfterInlineTag, "The character " + JSON.stringify(badCharacter) + " cannot appear after the TSDoc tag name; expecting a space");
return this._backtrackAndCreateErrorRangeForFailure(tokenReader, marker, atSignMarker, '', failure);
}
}
var done = false;
while (!done) {
switch (tokenReader.peekTokenKind()) {
case Token_1.TokenKind.EndOfInput:
return this._backtrackAndCreateErrorRange(tokenReader, marker, atSignMarker, TSDocMessageId_1.TSDocMessageId.InlineTagMissingRightBrace, 'The TSDoc inline tag name is missing its closing "}"');
case Token_1.TokenKind.Backslash:
// http://usejsdoc.org/about-block-inline-tags.html
// "If your tag's text includes a closing curly brace (}), you must escape it with
// a leading backslash (\)."
tokenReader.readToken(); // discard the backslash
// In CommonMark, a backslash is only allowed before a punctuation
// character. In all other contexts, the backslash is interpreted as a
// literal character.
if (!Tokenizer_1.Tokenizer.isPunctuation(tokenReader.peekTokenKind())) {
var failure = this._createFailureForToken(tokenReader, TSDocMessageId_1.TSDocMessageId.UnnecessaryBackslash, 'A backslash can only be used to escape a punctuation character');
return this._backtrackAndCreateErrorRangeForFailure(tokenReader, marker, atSignMarker, 'Error reading inline TSDoc tag: ', failure);
}
tokenReader.readToken();
break;
case Token_1.TokenKind.LeftCurlyBracket: {
var failure = this._createFailureForToken(tokenReader, TSDocMessageId_1.TSDocMessageId.InlineTagUnescapedBrace, 'The "{" character must be escaped with a backslash when used inside a TSDoc inline tag');
return this._backtrackAndCreateErrorRangeForFailure(tokenReader, marker, atSignMarker, '', failure);
}
case Token_1.TokenKind.RightCurlyBracket:
done = true;
break;
default:
tokenReader.readToken();
break;
}
}
var tagContentExcerpt = tokenReader.tryExtractAccumulatedSequence();
// Read the right curly bracket
tokenReader.readToken();
var closingDelimiterExcerpt = tokenReader.extractAccumulatedSequence();
var docInlineTagParsedParameters = {
parsed: true,
configuration: this._configuration,
openingDelimiterExcerpt: openingDelimiterExcerpt,
tagNameExcerpt: tagNameExcerpt,
tagName: tagName,
spacingAfterTagNameExcerpt: spacingAfterTagNameExcerpt,
tagContentExcerpt: tagContentExcerpt,
closingDelimiterExcerpt: closingDelimiterExcerpt
};
var tagNameWithUpperCase = tagName.toUpperCase();
// Create a new TokenReader that will reparse the tokens corresponding to the tagContent.
var embeddedTokenReader = new TokenReader_1.TokenReader(this._parserContext, tagContentExcerpt ? tagContentExcerpt : TokenSequence_1.TokenSequence.createEmpty(this._parserContext));
var docNode;
switch (tagNameWithUpperCase) {
case StandardTags_1.StandardTags.inheritDoc.tagNameWithUpperCase:
docNode = this._parseInheritDocTag(docInlineTagParsedParameters, embeddedTokenReader);
break;
case StandardTags_1.StandardTags.link.tagNameWithUpperCase:
docNode = this._parseLinkTag(docInlineTagParsedParameters, embeddedTokenReader);
break;
default:
docNode = new nodes_1.DocInlineTag(docInlineTagParsedParameters);
}
// Validate the tag
var tagDefinition = this._parserContext.configuration.tryGetTagDefinitionWithUpperCase(tagNameWithUpperCase);
this._validateTagDefinition(tagDefinition, tagName,
/* expectingInlineTag */ true, tagNameExcerpt, docNode);
return docNode;
};
NodeParser.prototype._parseInheritDocTag = function (docInlineTagParsedParameters, embeddedTokenReader) {
// If an error occurs, then return a generic DocInlineTag instead of DocInheritDocTag
var errorTag = new nodes_1.DocInlineTag(docInlineTagParsedParameters);
var parameters = __assign({}, docInlineTagParsedParameters);
if (embeddedTokenReader.peekTokenKind() !== Token_1.TokenKind.EndOfInput) {
parameters.declarationReference = this._parseDeclarationReference(embeddedTokenReader, docInlineTagParsedParameters.tagNameExcerpt, errorTag);
if (!parameters.declarationReference) {
return errorTag;
}
if (embeddedTokenReader.peekTokenKind() !== Token_1.TokenKind.EndOfInput) {
embeddedTokenReader.readToken();
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.InheritDocTagSyntax, 'Unexpected character after declaration reference', embeddedTokenReader.extractAccumulatedSequence(), errorTag);
return errorTag;
}
}
return new nodes_1.DocInheritDocTag(parameters);
};
NodeParser.prototype._parseLinkTag = function (docInlineTagParsedParameters, embeddedTokenReader) {
// If an error occurs, then return a generic DocInlineTag instead of DocInheritDocTag
var errorTag = new nodes_1.DocInlineTag(docInlineTagParsedParameters);
var parameters = __assign({}, docInlineTagParsedParameters);
if (!docInlineTagParsedParameters.tagContentExcerpt) {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.LinkTagEmpty, 'The @link tag content is missing', parameters.tagNameExcerpt, errorTag);
return errorTag;
}
// Is the link destination a URL or a declaration reference?
//
// The JSDoc "@link" tag allows URLs, however supporting full URLs would be highly
// ambiguous, for example "microsoft.windows.camera:" is an actual valid URI scheme,
// and even the common "mailto:example.com" looks suspiciously like a declaration reference.
// In practice JSDoc URLs are nearly always HTTP or HTTPS, so it seems fairly reasonable to
// require the URL to have "://" and a scheme without any punctuation in it. If a more exotic
// URL is needed, the HTML "<a>" tag can always be used.
// We start with a fairly broad classifier heuristic, and then the parsers will refine this:
// 1. Does it start with "//"?
// 2. Does it contain "://"?
var looksLikeUrl = embeddedTokenReader.peekTokenKind() === Token_1.TokenKind.Slash &&
embeddedTokenReader.peekTokenAfterKind() === Token_1.TokenKind.Slash;
var marker = embeddedTokenReader.createMarker();
var done = looksLikeUrl;
while (!done) {
switch (embeddedTokenReader.peekTokenKind()) {
// An URI scheme can contain letters, numbers, minus, plus, and periods
case Token_1.TokenKind.AsciiWord:
case Token_1.TokenKind.Period:
case Token_1.TokenKind.Hyphen:
case Token_1.TokenKind.Plus:
embeddedTokenReader.readToken();
break;
case Token_1.TokenKind.Colon:
embeddedTokenReader.readToken();
// Once we a reach a colon, then it's a URL only if we see "://"
looksLikeUrl =
embeddedTokenReader.peekTokenKind() === Token_1.TokenKind.Slash &&
embeddedTokenReader.peekTokenAfterKind() === Token_1.TokenKind.Slash;
done = true;
break;
default:
done = true;
}
}
embeddedTokenReader.backtrackToMarker(marker);
// Is the hyperlink a URL or a declaration reference?
if (looksLikeUrl) {
// It starts with something like "http://", so parse it as a URL
if (!this._parseLinkTagUrlDestination(embeddedTokenReader, parameters, docInlineTagParsedParameters.tagNameExcerpt, errorTag)) {
return errorTag;
}
}
else {
// Otherwise, assume it's a declaration reference
if (!this._parseLinkTagCodeDestination(embeddedTokenReader, parameters, docInlineTagParsedParameters.tagNameExcerpt, errorTag)) {
return errorTag;
}
}
if (embeddedTokenReader.peekTokenKind() === Token_1.TokenKind.Spacing) {
// The above parser rules should have consumed any spacing before the pipe
throw new Error('Unconsumed spacing encountered after construct');
}
if (embeddedTokenReader.peekTokenKind() === Token_1.TokenKind.Pipe) {
// Read the link text
embeddedTokenReader.readToken();
parameters.pipeExcerpt = embeddedTokenReader.extractAccumulatedSequence();
parameters.spacingAfterPipeExcerpt = this._tryReadSpacingAndNewlines(embeddedTokenReader);
// Read everything until the end
// NOTE: Because we're using an embedded TokenReader, the TokenKind.EndOfInput occurs
// when we reach the "}", not the end of the original input
done = false;
var spacingAfterLinkTextMarker = undefined;
while (!done) {
switch (embeddedTokenReader.peekTokenKind()) {
case Token_1.TokenKind.EndOfInput:
done = true;
break;
case Token_1.TokenKind.Pipe:
case Token_1.TokenKind.LeftCurlyBracket:
var badCharacter = embeddedTokenReader.readToken().toString();
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.LinkTagUnescapedText, "The \"" + badCharacter + "\" character may not be used in the link text without escaping it", embeddedTokenReader.extractAccumulatedSequence(), errorTag);
return errorTag;
case Token_1.TokenKind.Spacing:
case Token_1.TokenKind.Newline:
embeddedTokenReader.readToken();
break;
default:
// We found a non-spacing character, so move the spacingAfterLinkTextMarker
spacingAfterLinkTextMarker = embeddedTokenReader.createMarker() + 1;
embeddedTokenReader.readToken();
}
}
var linkTextAndSpacing = embeddedTokenReader.tryExtractAccumulatedSequence();
if (linkTextAndSpacing) {
if (spacingAfterLinkTextMarker === undefined) {
// We never found any non-spacing characters, so everything is trailing spacing
parameters.spacingAfterLinkTextExcerpt = linkTextAndSpacing;
}
else if (spacingAfterLinkTextMarker >= linkTextAndSpacing.endIndex) {
// We found no trailing spacing, so everything we found is the text
parameters.linkTextExcerpt = linkTextAndSpacing;
}
else {
// Split the trailing spacing from the link text
parameters.linkTextExcerpt = linkTextAndSpacing.getNewSequence(linkTextAndSpacing.startIndex, spacingAfterLinkTextMarker);
parameters.spacingAfterLinkTextExcerpt = linkTextAndSpacing.getNewSequence(spacingAfterLinkTextMarker, linkTextAndSpacing.endIndex);
}
}
}
else if (embeddedTokenReader.peekTokenKind() !== Token_1.TokenKind.EndOfInput) {
embeddedTokenReader.readToken();
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.LinkTagDestinationSyntax, 'Unexpected character after link destination', embeddedTokenReader.extractAccumulatedSequence(), errorTag);
return errorTag;
}
return new nodes_1.DocLinkTag(parameters);
};
NodeParser.prototype._parseLinkTagUrlDestination = function (embeddedTokenReader, parameters, tokenSequenceForErrorContext, nodeForErrorContext) {
// Simply accumulate everything up to the next space. We won't try to implement a proper
// URI parser here.
var urlDestination = '';
var done = false;
while (!done) {
switch (embeddedTokenReader.peekTokenKind()) {
case Token_1.TokenKind.Spacing:
case Token_1.TokenKind.Newline:
case Token_1.TokenKind.EndOfInput:
case Token_1.TokenKind.Pipe:
case Token_1.TokenKind.RightCurlyBracket:
done = true;
break;
default:
urlDestination += embeddedTokenReader.readToken();
break;
}
}
if (urlDestination.length === 0) {
// This should be impossible since the caller ensures that peekTokenKind() === TokenKind.AsciiWord
throw new Error('Missing URL in _parseLinkTagUrlDestination()');
}
var urlDestinationExcerpt = embeddedTokenReader.extractAccumulatedSequence();
var invalidUrlExplanation = StringChecks_1.StringChecks.explainIfInvalidLinkUrl(urlDestination);
if (invalidUrlExplanation) {
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.LinkTagInvalidUrl, invalidUrlExplanation, urlDestinationExcerpt, nodeForErrorContext);
return false;
}
parameters.urlDestinationExcerpt = urlDestinationExcerpt;
parameters.spacingAfterDestinationExcerpt = this._tryReadSpacingAndNewlines(embeddedTokenReader);
return true;
};
NodeParser.prototype._parseLinkTagCodeDestination = function (embeddedTokenReader, parameters, tokenSequenceForErrorContext, nodeForErrorContext) {
parameters.codeDestination = this._parseDeclarationReference(embeddedTokenReader, tokenSequenceForErrorContext, nodeForErrorContext);
return !!parameters.codeDestination;
};
NodeParser.prototype._parseDeclarationReference = function (tokenReader, tokenSequenceForErrorContext, nodeForErrorContext) {
tokenReader.assertAccumulatedSequenceIsEmpty();
// The package name can contain characters that look like a member reference. This means we need to scan forwards
// to see if there is a "#". However, we need to be careful not to match a "#" that is part of a quoted expression.
var marker = tokenReader.createMarker();
var hasHash = false;
// A common mistake is to forget the "#" for package name or import path. The telltale sign
// of this is mistake is that we see path-only characters such as "@" or "/" in the beginning
// where this would be a syntax error for a member reference.
var lookingForImportCharacters = true;
var sawImportCharacters = false;
var done = false;
while (!done) {
switch (tokenReader.peekTokenKind()) {
case Token_1.TokenKind.DoubleQuote:
case Token_1.TokenKind.EndOfInput:
case Token_1.TokenKind.LeftCurlyBracket:
case Token_1.TokenKind.LeftParenthesis:
case Token_1.TokenKind.LeftSquareBracket:
case Token_1.TokenKind.Newline:
case Token_1.TokenKind.Pipe:
case Token_1.TokenKind.RightCurlyBracket:
case Token_1.TokenKind.RightParenthesis:
case Token_1.TokenKind.RightSquareBracket:
case Token_1.TokenKind.SingleQuote:
case Token_1.TokenKind.Spacing:
done = true;
break;
case Token_1.TokenKind.PoundSymbol:
hasHash = true;
done = true;
break;
case Token_1.TokenKind.Slash:
case Token_1.TokenKind.AtSign:
if (lookingForImportCharacters) {
sawImportCharacters = true;
}
tokenReader.readToken();
break;
case Token_1.TokenKind.AsciiWord:
case Token_1.TokenKind.Period:
case Token_1.TokenKind.Hyphen:
// It's a character that looks like part of a package name or import path,
// so don't set lookingForImportCharacters = false
tokenReader.readToken();
break;
default:
// Once we reach something other than AsciiWord and Period, then the meaning of
// slashes and at-signs is no longer obvious.
lookingForImportCharacters = false;
tokenReader.readToken();
}
}
if (!hasHash && sawImportCharacters) {
// We saw characters that will be a syntax error if interpreted as a member reference,
// but would make sense as a package name or import path, but we did not find a "#"
this._parserContext.log.addMessageForTokenSequence(TSDocMessageId_1.TSDocMessageId.ReferenceMissingHash, 'The declaration reference appears to contain a package name or import path,' +
' but it is missing the "#" delimiter', tokenReader.extractAccumulatedSequence(), nodeForErrorContext);
return undefined;
}
tokenReader.backtrackToMarker(marker);
var packageNameExcerpt;
var importPathExcerpt;
var importHashExcerpt;
var spacingAfterImportHashExcerpt;
if (hasHash) {
// If it starts with a "." then it's a relative path, not a package name
if (tokenReader.peekTokenKind() !== Token_1.TokenKind.Period)