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
});
}