private _parseDeclarationReference()

in tsdoc/src/parser/NodeParser.ts [1254:1499]


  private _parseDeclarationReference(
    tokenReader: TokenReader,
    tokenSequenceForErrorContext: TokenSequence,
    nodeForErrorContext: DocNode
  ): DocDeclarationReference | undefined {
    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.

    const marker: number = tokenReader.createMarker();
    let hasHash: boolean = 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.
    let lookingForImportCharacters: boolean = true;
    let sawImportCharacters: boolean = false;

    let done: boolean = false;
    while (!done) {
      switch (tokenReader.peekTokenKind()) {
        case TokenKind.DoubleQuote:
        case TokenKind.EndOfInput:
        case TokenKind.LeftCurlyBracket:
        case TokenKind.LeftParenthesis:
        case TokenKind.LeftSquareBracket:
        case TokenKind.Newline:
        case TokenKind.Pipe:
        case TokenKind.RightCurlyBracket:
        case TokenKind.RightParenthesis:
        case TokenKind.RightSquareBracket:
        case TokenKind.SingleQuote:
        case TokenKind.Spacing:
          done = true;
          break;
        case TokenKind.PoundSymbol:
          hasHash = true;
          done = true;
          break;
        case TokenKind.Slash:
        case TokenKind.AtSign:
          if (lookingForImportCharacters) {
            sawImportCharacters = true;
          }
          tokenReader.readToken();
          break;
        case TokenKind.AsciiWord:
        case TokenKind.Period:
        case 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.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);

    let packageNameExcerpt: TokenSequence | undefined;
    let importPathExcerpt: TokenSequence | undefined;
    let importHashExcerpt: TokenSequence | undefined;
    let spacingAfterImportHashExcerpt: TokenSequence | undefined;

    if (hasHash) {
      // If it starts with a "." then it's a relative path, not a package name
      if (tokenReader.peekTokenKind() !== TokenKind.Period) {
        // Read the package name:
        const scopedPackageName: boolean = tokenReader.peekTokenKind() === TokenKind.AtSign;
        let finishedScope: boolean = false;

        done = false;
        while (!done) {
          switch (tokenReader.peekTokenKind()) {
            case TokenKind.EndOfInput:
              // If hasHash=true, then we are expecting to stop when we reach the hash
              throw new Error('Expecting pound symbol');
            case TokenKind.Slash:
              // Stop at the first slash, unless this is a scoped package, in which case we stop at the second slash
              if (scopedPackageName && !finishedScope) {
                tokenReader.readToken();
                finishedScope = true;
              } else {
                done = true;
              }
              break;
            case TokenKind.PoundSymbol:
              done = true;
              break;
            default:
              tokenReader.readToken();
          }
        }

        if (!tokenReader.isAccumulatedSequenceEmpty()) {
          packageNameExcerpt = tokenReader.extractAccumulatedSequence();

          // Check that the packageName is syntactically valid
          const explanation: string | undefined = StringChecks.explainIfInvalidPackageName(
            packageNameExcerpt.toString()
          );
          if (explanation) {
            this._parserContext.log.addMessageForTokenSequence(
              TSDocMessageId.ReferenceMalformedPackageName,
              explanation,
              packageNameExcerpt,
              nodeForErrorContext
            );
            return undefined;
          }
        }
      }

      // Read the import path:
      done = false;
      while (!done) {
        switch (tokenReader.peekTokenKind()) {
          case TokenKind.EndOfInput:
            // If hasHash=true, then we are expecting to stop when we reach the hash
            throw new Error('Expecting pound symbol');
          case TokenKind.PoundSymbol:
            done = true;
            break;
          default:
            tokenReader.readToken();
        }
      }

      if (!tokenReader.isAccumulatedSequenceEmpty()) {
        importPathExcerpt = tokenReader.extractAccumulatedSequence();

        // Check that the importPath is syntactically valid
        const explanation: string | undefined = StringChecks.explainIfInvalidImportPath(
          importPathExcerpt.toString(),
          !!packageNameExcerpt
        );
        if (explanation) {
          this._parserContext.log.addMessageForTokenSequence(
            TSDocMessageId.ReferenceMalformedImportPath,
            explanation,
            importPathExcerpt,
            nodeForErrorContext
          );
          return undefined;
        }
      }

      // Read the import hash
      if (tokenReader.peekTokenKind() !== TokenKind.PoundSymbol) {
        // The above logic should have left us at the PoundSymbol
        throw new Error('Expecting pound symbol');
      }
      tokenReader.readToken();
      importHashExcerpt = tokenReader.extractAccumulatedSequence();

      spacingAfterImportHashExcerpt = this._tryReadSpacingAndNewlines(tokenReader);

      if (packageNameExcerpt === undefined && importPathExcerpt === undefined) {
        this._parserContext.log.addMessageForTokenSequence(
          TSDocMessageId.ReferenceHashSyntax,
          'The hash character must be preceded by a package name or import path',
          importHashExcerpt,
          nodeForErrorContext
        );
        return undefined;
      }
    }

    // Read the member references:
    const memberReferences: DocMemberReference[] = [];

    done = false;
    while (!done) {
      switch (tokenReader.peekTokenKind()) {
        case TokenKind.Period:
        case TokenKind.LeftParenthesis:
        case TokenKind.AsciiWord:
        case TokenKind.Colon:
        case TokenKind.LeftSquareBracket:
        case TokenKind.DoubleQuote:
          const expectingDot: boolean = memberReferences.length > 0;
          const memberReference: DocMemberReference | undefined = this._parseMemberReference(
            tokenReader,
            expectingDot,
            tokenSequenceForErrorContext,
            nodeForErrorContext
          );

          if (!memberReference) {
            return undefined;
          }

          memberReferences.push(memberReference);
          break;
        default:
          done = true;
      }
    }

    if (
      packageNameExcerpt === undefined &&
      importPathExcerpt === undefined &&
      memberReferences.length === 0
    ) {
      // We didn't find any parts of a declaration reference
      this._parserContext.log.addMessageForTokenSequence(
        TSDocMessageId.MissingReference,
        'Expecting a declaration reference',
        tokenSequenceForErrorContext,
        nodeForErrorContext
      );
      return undefined;
    }

    return new DocDeclarationReference({
      parsed: true,
      configuration: this._configuration,

      packageNameExcerpt,
      importPathExcerpt,

      importHashExcerpt,
      spacingAfterImportHashExcerpt,

      memberReferences
    });
  }