UNPKG

@microsoft/tsdoc

Version:

A parser for the TypeScript doc comment syntax

842 lines 94.6 kB
"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)