1206 lines
40 KiB
Go
1206 lines
40 KiB
Go
package parser
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
|
|
)
|
|
|
|
type jsdocState int32
|
|
|
|
const (
|
|
jsdocStateBeginningOfLine jsdocState = iota
|
|
jsdocStateSawAsterisk
|
|
jsdocStateSavingComments
|
|
jsdocStateSavingBackticks
|
|
)
|
|
|
|
type propertyLikeParse int32
|
|
|
|
const (
|
|
propertyLikeParseProperty propertyLikeParse = 1 << iota
|
|
propertyLikeParseParameter
|
|
propertyLikeParseCallbackParameter
|
|
)
|
|
|
|
func (p *Parser) withJSDoc(node *ast.Node, hasJSDoc bool) []*ast.Node {
|
|
if !hasJSDoc {
|
|
return nil
|
|
}
|
|
|
|
if p.jsdocCache == nil {
|
|
p.jsdocCache = make(map[*ast.Node][]*ast.Node, strings.Count(p.sourceText, "/**"))
|
|
} else if _, ok := p.jsdocCache[node]; ok {
|
|
panic("tried to set JSDoc on a node with existing JSDoc")
|
|
}
|
|
// Should only be called once per node
|
|
p.hasDeprecatedTag = false
|
|
ranges := GetJSDocCommentRanges(&p.factory, p.jsdocCommentRangesSpace, node, p.sourceText)
|
|
p.jsdocCommentRangesSpace = ranges[:0]
|
|
jsdoc := p.nodeSlicePool.NewSlice(len(ranges))[:0]
|
|
pos := node.Pos()
|
|
for _, comment := range ranges {
|
|
if parsed := p.parseJSDocComment(node, comment.Pos(), comment.End(), pos); parsed != nil {
|
|
parsed.Parent = node
|
|
jsdoc = append(jsdoc, parsed)
|
|
pos = parsed.End()
|
|
}
|
|
}
|
|
if len(jsdoc) != 0 {
|
|
if node.Flags&ast.NodeFlagsHasJSDoc == 0 {
|
|
node.Flags |= ast.NodeFlagsHasJSDoc
|
|
}
|
|
if p.hasDeprecatedTag {
|
|
p.hasDeprecatedTag = false
|
|
node.Flags |= ast.NodeFlagsDeprecated
|
|
}
|
|
if p.scriptKind == core.ScriptKindJS || p.scriptKind == core.ScriptKindJSX {
|
|
p.reparseTags(node, jsdoc)
|
|
}
|
|
p.jsdocCache[node] = jsdoc
|
|
return jsdoc
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) parseJSDocTypeExpression(mayOmitBraces bool) *ast.Node {
|
|
pos := p.nodePos()
|
|
var hasBrace bool
|
|
if mayOmitBraces {
|
|
hasBrace = p.parseOptional(ast.KindOpenBraceToken)
|
|
} else {
|
|
hasBrace = p.parseExpected(ast.KindOpenBraceToken)
|
|
}
|
|
saveContextFlags := p.contextFlags
|
|
p.setContextFlags(ast.NodeFlagsJSDoc, true)
|
|
t := p.parseJSDocType()
|
|
p.contextFlags = saveContextFlags
|
|
if hasBrace {
|
|
p.parseExpectedJSDoc(ast.KindCloseBraceToken)
|
|
}
|
|
|
|
return p.finishNode(p.factory.NewJSDocTypeExpression(t), pos)
|
|
}
|
|
|
|
func (p *Parser) parseJSDocNameReference() *ast.Node {
|
|
pos := p.nodePos()
|
|
hasBrace := p.parseOptional(ast.KindOpenBraceToken)
|
|
p2 := p.nodePos()
|
|
entityName := p.parseEntityName(false, nil)
|
|
for p.token == ast.KindPrivateIdentifier {
|
|
p.scanner.ReScanHashToken() // rescan #id as # id
|
|
p.nextTokenJSDoc() // then skip the #
|
|
entityName = p.finishNode(p.factory.NewQualifiedName(entityName, p.parseIdentifier()), p2)
|
|
}
|
|
if hasBrace {
|
|
p.parseExpectedJSDoc(ast.KindCloseBraceToken)
|
|
}
|
|
|
|
return p.finishNode(p.factory.NewJSDocNameReference(entityName), pos)
|
|
}
|
|
|
|
// Pass end=-1 to parse the text to the end
|
|
func (p *Parser) parseJSDocComment(parent *ast.Node, start int, end int, fullStart int) *ast.Node {
|
|
if end == -1 {
|
|
end = len(p.sourceText)
|
|
}
|
|
// Check for /** (JSDoc opening part)
|
|
if !isJSDocLikeText(p.sourceText[start:]) {
|
|
// TODO: This should be a panic, unless parseSingleJSDocComment is calling this (not ported yet)
|
|
return nil
|
|
}
|
|
|
|
saveSourceText := p.sourceText
|
|
saveToken := p.token
|
|
saveContextFlags := p.contextFlags
|
|
saveParsingContexts := p.parsingContexts
|
|
saveScannerState := p.scanner.Mark()
|
|
saveDiagnosticsLength := len(p.diagnostics)
|
|
saveHasParseError := p.hasParseError
|
|
saveHasAwaitIdentifier := p.statementHasAwaitIdentifier
|
|
|
|
// initial indent is start+4 to account for leading `/** `
|
|
// + 1 because \n is one character before the first character in the line and,
|
|
// if there is no \n before start, -1 is one index before the first character in the string
|
|
initialIndent := start + 4 - (strings.LastIndex(p.sourceText[:start], "\n") + 1)
|
|
// -2 for trailing `*/`
|
|
p.sourceText = p.sourceText[:end-2]
|
|
p.scanner.SetText(p.sourceText)
|
|
// +3 for leading `/**`
|
|
p.scanner.ResetPos(start + 3)
|
|
p.setContextFlags(ast.NodeFlagsJSDoc, true)
|
|
p.parsingContexts = p.parsingContexts | ParsingContexts(PCJSDocComment)
|
|
|
|
comment := p.parseJSDocCommentWorker(start, end, fullStart, initialIndent)
|
|
// move jsdoc diagnostics to jsdocDiagnostics -- for JS files only
|
|
if p.contextFlags&ast.NodeFlagsJavaScriptFile != 0 {
|
|
p.jsdocDiagnostics = append(p.jsdocDiagnostics, p.diagnostics[saveDiagnosticsLength:]...)
|
|
}
|
|
p.diagnostics = p.diagnostics[0:saveDiagnosticsLength]
|
|
|
|
p.sourceText = saveSourceText
|
|
p.scanner.SetText(p.sourceText)
|
|
p.parsingContexts = saveParsingContexts
|
|
p.contextFlags = saveContextFlags
|
|
p.scanner.Rewind(saveScannerState)
|
|
p.token = saveToken
|
|
p.hasParseError = saveHasParseError
|
|
p.statementHasAwaitIdentifier = saveHasAwaitIdentifier
|
|
|
|
return comment
|
|
}
|
|
|
|
/**
|
|
* @param offset - the offset in the containing file
|
|
* @param indent - the number of spaces to consider as the margin (applies to non-first lines only)
|
|
*/
|
|
func (p *Parser) parseJSDocCommentWorker(start int, end int, fullStart int, indent int) *ast.Node {
|
|
// Initially we can parse out a tag. We also have seen a starting asterisk.
|
|
// This is so that /** * @type */ doesn't parse.
|
|
tags := p.nodeSlicePool.NewSlice(1)[:0]
|
|
tagsPos := -1
|
|
tagsEnd := -1
|
|
state := jsdocStateSawAsterisk
|
|
commentParts := p.nodeSlicePool.NewSlice(1)[:0]
|
|
comments := p.jsdocCommentsSpace
|
|
commentsPos := -1
|
|
linkEnd := start
|
|
margin := -1
|
|
pushComment := func(text string) {
|
|
if margin == -1 {
|
|
margin = indent
|
|
}
|
|
comments = append(comments, text)
|
|
indent += len(text)
|
|
}
|
|
|
|
p.nextTokenJSDoc()
|
|
for p.parseOptionalJsdoc(ast.KindWhitespaceTrivia) {
|
|
}
|
|
if p.parseOptionalJsdoc(ast.KindNewLineTrivia) {
|
|
state = jsdocStateBeginningOfLine
|
|
indent = 0
|
|
}
|
|
loop:
|
|
for {
|
|
switch p.token {
|
|
case ast.KindAtToken:
|
|
comments = removeTrailingWhitespace(comments)
|
|
if commentsPos == -1 {
|
|
commentsPos = p.nodePos()
|
|
}
|
|
tag := p.parseTag(tags, indent)
|
|
if tagsPos == -1 {
|
|
tagsPos = tag.Pos()
|
|
}
|
|
tags = append(tags, tag)
|
|
tagsEnd = tag.End()
|
|
// NOTE: According to usejsdoc.org, a tag goes to end of line, except the last tag.
|
|
// Real-world comments may break this rule, so "BeginningOfLine" will not be a real line beginning
|
|
// for malformed examples like `/** @param {string} x @returns {number} the length */`
|
|
state = jsdocStateBeginningOfLine
|
|
margin = -1
|
|
case ast.KindNewLineTrivia:
|
|
comments = append(comments, p.scanner.TokenText())
|
|
state = jsdocStateBeginningOfLine
|
|
indent = 0
|
|
case ast.KindAsteriskToken:
|
|
asterisk := p.scanner.TokenText()
|
|
if state == jsdocStateSawAsterisk {
|
|
// If we've already seen an asterisk, then we can no longer parse a tag on this line
|
|
state = jsdocStateSavingComments
|
|
pushComment(asterisk)
|
|
} else {
|
|
if state != jsdocStateBeginningOfLine {
|
|
panic("state must be BeginningOfLine")
|
|
}
|
|
// Ignore the first asterisk on a line
|
|
state = jsdocStateSawAsterisk
|
|
indent += len(asterisk)
|
|
}
|
|
case ast.KindWhitespaceTrivia:
|
|
if state == jsdocStateSavingComments {
|
|
panic("whitespace shouldn't come from the scanner while saving top-level comment text")
|
|
}
|
|
// only collect whitespace if we're already saving comments or have just crossed the comment indent margin
|
|
whitespace := p.scanner.TokenText()
|
|
if margin > -1 && indent+len(whitespace) > margin {
|
|
existingIndent := margin - indent
|
|
if existingIndent < 0 {
|
|
existingIndent += len(whitespace)
|
|
}
|
|
if existingIndent < 0 {
|
|
existingIndent = 0
|
|
}
|
|
comments = append(comments, whitespace[existingIndent:])
|
|
}
|
|
indent += len(whitespace)
|
|
case ast.KindEndOfFile:
|
|
break loop
|
|
case ast.KindJSDocCommentTextToken:
|
|
state = jsdocStateSavingComments
|
|
pushComment(p.scanner.TokenValue())
|
|
case ast.KindOpenBraceToken:
|
|
state = jsdocStateSavingComments
|
|
commentEnd := p.scanner.TokenFullStart()
|
|
linkStart := p.scanner.TokenEnd() - 1
|
|
link := p.parseJSDocLink(linkStart)
|
|
if link != nil {
|
|
if linkEnd == start {
|
|
comments = removeLeadingNewlines(comments)
|
|
}
|
|
jsdocText := p.finishNodeWithEnd(p.factory.NewJSDocText(p.stringSlicePool.Clone(comments)), linkEnd, commentEnd)
|
|
commentParts = append(commentParts, jsdocText, link)
|
|
comments = comments[:0]
|
|
linkEnd = p.scanner.TokenEnd()
|
|
break
|
|
}
|
|
fallthrough
|
|
default:
|
|
// Anything else is doc comment text. We just save it. Because it
|
|
// wasn't a tag, we can no longer parse a tag on this line until we hit the next
|
|
// line break.
|
|
state = jsdocStateSavingComments
|
|
pushComment(p.scanner.TokenText())
|
|
}
|
|
if state == jsdocStateSavingComments {
|
|
p.nextJSDocCommentTextToken(false)
|
|
} else {
|
|
p.nextTokenJSDoc()
|
|
}
|
|
}
|
|
|
|
p.jsdocCommentsSpace = comments[:0] // Reuse this slice for further parses
|
|
if commentsPos == -1 {
|
|
commentsPos = p.scanner.TokenFullStart()
|
|
}
|
|
|
|
if len(comments) > 0 {
|
|
comments[len(comments)-1] = strings.TrimRightFunc(comments[len(comments)-1], unicode.IsSpace)
|
|
jsdocText := p.finishNodeWithEnd(p.factory.NewJSDocText(p.stringSlicePool.Clone(comments)), linkEnd, commentsPos)
|
|
commentParts = append(commentParts, jsdocText)
|
|
}
|
|
|
|
if len(commentParts) > 0 && len(tags) > 0 && commentsPos == -1 {
|
|
panic("having parsed tags implies that the end of the comment span should be set")
|
|
}
|
|
|
|
var tagsNodeList *ast.NodeList
|
|
if tagsPos != -1 {
|
|
tagsNodeList = p.newNodeList(core.NewTextRange(tagsPos, tagsEnd), tags)
|
|
}
|
|
|
|
jsdocComment := p.factory.NewJSDoc(
|
|
p.newNodeList(core.NewTextRange(start, commentsPos), commentParts),
|
|
tagsNodeList,
|
|
)
|
|
return p.finishNodeWithEnd(jsdocComment, fullStart, end)
|
|
}
|
|
|
|
func removeLeadingNewlines(comments []string) []string {
|
|
i := 0
|
|
for i < len(comments) && (comments[i] == "\n" || comments[i] == "\r") {
|
|
i++
|
|
}
|
|
return comments[i:]
|
|
}
|
|
|
|
func trimEnd(s string) string {
|
|
return strings.TrimRightFunc(s, stringutil.IsWhiteSpaceLike)
|
|
}
|
|
|
|
func removeTrailingWhitespace(comments []string) []string {
|
|
end := len(comments)
|
|
for i := len(comments) - 1; i >= 0; i-- {
|
|
trimmed := trimEnd(comments[i])
|
|
if trimmed == "" {
|
|
end = i
|
|
} else {
|
|
comments[i] = trimmed
|
|
break
|
|
}
|
|
}
|
|
return comments[:end]
|
|
}
|
|
|
|
func (p *Parser) isNextNonwhitespaceTokenEndOfFile() bool {
|
|
// We must use infinite lookahead, as there could be any number of newlines :(
|
|
for {
|
|
p.nextTokenJSDoc()
|
|
if p.token == ast.KindEndOfFile {
|
|
return true
|
|
}
|
|
if !(p.token == ast.KindWhitespaceTrivia || p.token == ast.KindNewLineTrivia) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Parser) skipWhitespace() {
|
|
if p.token == ast.KindWhitespaceTrivia || p.token == ast.KindNewLineTrivia {
|
|
if p.lookAhead((*Parser).isNextNonwhitespaceTokenEndOfFile) {
|
|
return
|
|
// Don't skip whitespace prior to EoF (or end of comment) - that shouldn't be included in any node's range
|
|
}
|
|
}
|
|
for p.token == ast.KindWhitespaceTrivia || p.token == ast.KindNewLineTrivia {
|
|
p.nextTokenJSDoc()
|
|
}
|
|
}
|
|
|
|
func (p *Parser) skipWhitespaceOrAsterisk() string {
|
|
if p.token == ast.KindWhitespaceTrivia || p.token == ast.KindNewLineTrivia {
|
|
if p.lookAhead((*Parser).isNextNonwhitespaceTokenEndOfFile) {
|
|
return ""
|
|
// Don't skip whitespace prior to EoF (or end of comment) - that shouldn't be included in any node's range
|
|
}
|
|
}
|
|
|
|
precedingLineBreak := p.scanner.HasPrecedingLineBreak()
|
|
seenLineBreak := false
|
|
indents := make([]string, 0, 4)
|
|
for (precedingLineBreak && p.token == ast.KindAsteriskToken) || p.token == ast.KindWhitespaceTrivia || p.token == ast.KindNewLineTrivia {
|
|
indents = append(indents, p.scanner.TokenText())
|
|
if p.token == ast.KindNewLineTrivia {
|
|
precedingLineBreak = true
|
|
seenLineBreak = true
|
|
indents = indents[:0]
|
|
} else if p.token == ast.KindAsteriskToken {
|
|
precedingLineBreak = false
|
|
}
|
|
p.nextTokenJSDoc()
|
|
}
|
|
if seenLineBreak {
|
|
return strings.Join(indents, "")
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseTag(tags []*ast.Node, margin int) *ast.Node {
|
|
if p.token != ast.KindAtToken {
|
|
panic("should be called only at the start of a tag")
|
|
}
|
|
start := p.scanner.TokenStart()
|
|
p.nextTokenJSDoc()
|
|
|
|
tagName := p.parseJSDocIdentifierName(nil)
|
|
indentText := p.skipWhitespaceOrAsterisk()
|
|
|
|
var tag *ast.Node
|
|
switch tagName.Text() {
|
|
case "implements":
|
|
tag = p.parseImplementsTag(start, tagName, margin, indentText)
|
|
case "augments", "extends":
|
|
tag = p.parseAugmentsTag(start, tagName, margin, indentText)
|
|
case "public":
|
|
tag = p.parseSimpleTag(start, func(tagName *ast.IdentifierNode, comments *ast.NodeList) *ast.Node {
|
|
return p.factory.NewJSDocPublicTag(tagName, comments)
|
|
}, tagName, margin, indentText)
|
|
case "private":
|
|
tag = p.parseSimpleTag(start, func(tagName *ast.IdentifierNode, comments *ast.NodeList) *ast.Node {
|
|
return p.factory.NewJSDocPrivateTag(tagName, comments)
|
|
}, tagName, margin, indentText)
|
|
case "protected":
|
|
tag = p.parseSimpleTag(start, func(tagName *ast.IdentifierNode, comments *ast.NodeList) *ast.Node {
|
|
return p.factory.NewJSDocProtectedTag(tagName, comments)
|
|
}, tagName, margin, indentText)
|
|
case "readonly":
|
|
tag = p.parseSimpleTag(start, func(tagName *ast.IdentifierNode, comments *ast.NodeList) *ast.Node {
|
|
return p.factory.NewJSDocReadonlyTag(tagName, comments)
|
|
}, tagName, margin, indentText)
|
|
case "override":
|
|
tag = p.parseSimpleTag(start, func(tagName *ast.IdentifierNode, comments *ast.NodeList) *ast.Node {
|
|
return p.factory.NewJSDocOverrideTag(tagName, comments)
|
|
}, tagName, margin, indentText)
|
|
case "deprecated":
|
|
p.hasDeprecatedTag = true
|
|
tag = p.parseSimpleTag(start, func(tagName *ast.IdentifierNode, comments *ast.NodeList) *ast.Node {
|
|
return p.factory.NewJSDocDeprecatedTag(tagName, comments)
|
|
}, tagName, margin, indentText)
|
|
case "this":
|
|
tag = p.parseThisTag(start, tagName, margin, indentText)
|
|
case "arg", "argument", "param":
|
|
tag = p.parseParameterOrPropertyTag(start, tagName, propertyLikeParseParameter, margin)
|
|
case "return", "returns":
|
|
tag = p.parseReturnTag(tags, start, tagName, margin, indentText)
|
|
case "template":
|
|
tag = p.parseTemplateTag(start, tagName, margin, indentText)
|
|
case "type":
|
|
tag = p.parseTypeTag(tags, start, tagName, margin, indentText)
|
|
case "typedef":
|
|
tag = p.parseTypedefTag(start, tagName, margin, indentText)
|
|
case "callback":
|
|
tag = p.parseCallbackTag(start, tagName, margin, indentText)
|
|
case "overload":
|
|
tag = p.parseOverloadTag(start, tagName, margin, indentText)
|
|
case "satisfies":
|
|
tag = p.parseSatisfiesTag(start, tagName, margin, indentText)
|
|
case "see":
|
|
tag = p.parseSeeTag(start, tagName, margin, indentText)
|
|
case "import":
|
|
tag = p.parseImportTag(start, tagName, margin, indentText)
|
|
default:
|
|
tag = p.parseUnknownTag(start, tagName, margin, indentText)
|
|
}
|
|
if tag == nil {
|
|
panic("tag should not be nil")
|
|
}
|
|
return tag
|
|
}
|
|
|
|
func (p *Parser) parseTrailingTagComments(pos int, end int, margin int, indentText string) *ast.NodeList {
|
|
// some tags, like typedef and callback, have already parsed their comments earlier
|
|
if len(indentText) == 0 {
|
|
margin += end - pos
|
|
}
|
|
var initialMargin string
|
|
if margin < len(indentText) {
|
|
initialMargin = indentText[margin:]
|
|
}
|
|
return p.parseTagComments(margin, &initialMargin)
|
|
}
|
|
|
|
func (p *Parser) parseTagComments(indent int, initialMargin *string) *ast.NodeList {
|
|
commentsPos := p.nodePos()
|
|
comments := p.jsdocTagCommentsSpace
|
|
p.jsdocTagCommentsSpace = nil // !!! can parseTagComments call itself?
|
|
parts := p.jsdocTagCommentsPartsSpace
|
|
p.jsdocTagCommentsPartsSpace = nil
|
|
linkEnd := -1
|
|
state := jsdocStateBeginningOfLine
|
|
if indent < 0 {
|
|
panic("indent must be a natural number")
|
|
}
|
|
margin := -1
|
|
pushComment := func(text string) {
|
|
if margin == -1 {
|
|
margin = indent
|
|
}
|
|
comments = append(comments, text)
|
|
indent += len(text)
|
|
}
|
|
|
|
if initialMargin != nil {
|
|
// jump straight to saving comments if there is some initial indentation
|
|
if *initialMargin != "" {
|
|
pushComment(*initialMargin)
|
|
}
|
|
state = jsdocStateSawAsterisk
|
|
}
|
|
tok := p.token
|
|
loop:
|
|
for {
|
|
switch tok {
|
|
case ast.KindNewLineTrivia:
|
|
state = jsdocStateBeginningOfLine
|
|
// don't use pushComment here because we want to keep the margin unchanged
|
|
comments = append(comments, p.scanner.TokenText())
|
|
indent = 0
|
|
case ast.KindAtToken:
|
|
p.scanner.ResetPos(p.scanner.TokenEnd() - 1)
|
|
break loop
|
|
case ast.KindEndOfFile:
|
|
// Done
|
|
break loop
|
|
case ast.KindWhitespaceTrivia:
|
|
if state == jsdocStateSavingComments || state == jsdocStateSavingBackticks {
|
|
panic("whitespace shouldn't come from the scanner while saving comment text")
|
|
}
|
|
whitespace := p.scanner.TokenText()
|
|
// if the whitespace crosses the margin, take only the whitespace that passes the margin
|
|
if margin > -1 && indent+len(whitespace) > margin {
|
|
comments = append(comments, whitespace[max(margin-indent, 0):])
|
|
state = jsdocStateSavingComments
|
|
}
|
|
indent += len(whitespace)
|
|
case ast.KindOpenBraceToken:
|
|
state = jsdocStateSavingComments
|
|
commentEnd := p.scanner.TokenFullStart()
|
|
linkStart := p.scanner.TokenEnd() - 1
|
|
link := p.parseJSDocLink(linkStart)
|
|
if link != nil {
|
|
var commentStart int
|
|
if linkEnd > -1 {
|
|
commentStart = linkEnd
|
|
} else {
|
|
commentStart = commentsPos
|
|
}
|
|
text := p.finishNodeWithEnd(p.factory.NewJSDocText(p.stringSlicePool.Clone(comments)), commentStart, commentEnd)
|
|
parts = append(parts, text)
|
|
parts = append(parts, link)
|
|
comments = comments[:0]
|
|
linkEnd = p.scanner.TokenEnd()
|
|
} else {
|
|
pushComment(p.scanner.TokenText())
|
|
}
|
|
case ast.KindBacktickToken:
|
|
if state == jsdocStateSavingBackticks {
|
|
state = jsdocStateSavingComments
|
|
} else {
|
|
state = jsdocStateSavingBackticks
|
|
}
|
|
pushComment(p.scanner.TokenText())
|
|
case ast.KindJSDocCommentTextToken:
|
|
if state != jsdocStateSavingBackticks {
|
|
state = jsdocStateSavingComments
|
|
// leading identifiers start recording as well
|
|
}
|
|
pushComment(p.scanner.TokenValue())
|
|
case ast.KindAsteriskToken:
|
|
if state == jsdocStateBeginningOfLine {
|
|
// leading asterisks start recording on the *next* (non-whitespace) token
|
|
state = jsdocStateSawAsterisk
|
|
indent += 1
|
|
break
|
|
}
|
|
// record the * as a comment
|
|
fallthrough
|
|
default:
|
|
if state != jsdocStateSavingBackticks {
|
|
state = jsdocStateSavingComments
|
|
// leading identifiers start recording as well
|
|
}
|
|
pushComment(p.scanner.TokenText())
|
|
}
|
|
if state == jsdocStateSavingComments || state == jsdocStateSavingBackticks {
|
|
tok = p.nextJSDocCommentTextToken(state == jsdocStateSavingBackticks)
|
|
} else {
|
|
tok = p.nextTokenJSDoc()
|
|
}
|
|
}
|
|
|
|
p.jsdocTagCommentsSpace = comments[:0]
|
|
|
|
comments = removeLeadingNewlines(comments)
|
|
if len(comments) > 0 {
|
|
var commentStart int
|
|
if linkEnd > -1 {
|
|
commentStart = linkEnd
|
|
} else {
|
|
commentStart = commentsPos
|
|
}
|
|
text := p.finishNode(p.factory.NewJSDocText(p.stringSlicePool.Clone(comments)), commentStart)
|
|
parts = append(parts, text)
|
|
}
|
|
|
|
p.jsdocTagCommentsPartsSpace = parts[:0]
|
|
|
|
if len(parts) > 0 {
|
|
return p.newNodeList(core.NewTextRange(commentsPos, p.scanner.TokenEnd()), p.nodeSlicePool.Clone(parts))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) parseJSDocLink(start int) *ast.Node {
|
|
state := p.mark()
|
|
linkType, ok := p.parseJSDocLinkPrefix()
|
|
if !ok {
|
|
p.rewind(state)
|
|
return nil
|
|
}
|
|
p.nextTokenJSDoc()
|
|
// start at token after link, then skip any whitespace
|
|
p.skipWhitespace()
|
|
name := p.parseJSDocLinkName()
|
|
var text []string
|
|
for p.token != ast.KindCloseBraceToken && p.token != ast.KindNewLineTrivia && p.token != ast.KindEndOfFile {
|
|
text = append(text, p.scanner.TokenText())
|
|
p.nextTokenJSDoc() // Couldn't this be nextTokenCommentJSDoc?
|
|
}
|
|
var create *ast.Node
|
|
switch linkType {
|
|
case "link":
|
|
create = p.factory.NewJSDocLink(name, text)
|
|
case "linkcode":
|
|
create = p.factory.NewJSDocLinkCode(name, text)
|
|
default:
|
|
create = p.factory.NewJSDocLinkPlain(name, text)
|
|
}
|
|
return p.finishNodeWithEnd(create, start, p.scanner.TokenEnd())
|
|
}
|
|
|
|
func (p *Parser) parseJSDocLinkName() *ast.Node {
|
|
if tokenIsIdentifierOrKeyword(p.token) {
|
|
pos := p.nodePos()
|
|
|
|
name := p.parseIdentifierName()
|
|
for p.parseOptional(ast.KindDotToken) {
|
|
var right *ast.IdentifierNode
|
|
if p.token == ast.KindPrivateIdentifier {
|
|
right = p.createMissingIdentifier()
|
|
} else {
|
|
right = p.parseIdentifierName()
|
|
}
|
|
name = p.finishNode(p.factory.NewQualifiedName(name, right), pos)
|
|
}
|
|
|
|
for p.token == ast.KindPrivateIdentifier {
|
|
p.scanner.ReScanHashToken()
|
|
p.nextTokenJSDoc()
|
|
name = p.finishNode(p.factory.NewQualifiedName(name, p.parseIdentifier()), pos)
|
|
}
|
|
return name
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) parseJSDocLinkPrefix() (string, bool) {
|
|
p.skipWhitespaceOrAsterisk()
|
|
if p.token == ast.KindOpenBraceToken && p.nextTokenJSDoc() == ast.KindAtToken && tokenIsIdentifierOrKeyword(p.nextTokenJSDoc()) {
|
|
kind := p.scanner.TokenValue()
|
|
if isJSDocLinkTag(kind) {
|
|
return kind, true
|
|
}
|
|
}
|
|
return "NONE", false
|
|
}
|
|
|
|
func isJSDocLinkTag(kind string) bool {
|
|
return kind == "link" || kind == "linkcode" || kind == "linkplain"
|
|
}
|
|
|
|
func (p *Parser) parseUnknownTag(start int, tagName *ast.IdentifierNode, indent int, indentText string) *ast.Node {
|
|
return p.finishNode(p.factory.NewJSDocUnknownTag(tagName, p.parseTrailingTagComments(start, p.nodePos(), indent, indentText)), start)
|
|
}
|
|
|
|
func (p *Parser) tryParseTypeExpression() *ast.Node {
|
|
p.skipWhitespaceOrAsterisk()
|
|
if p.token == ast.KindOpenBraceToken {
|
|
return p.parseJSDocTypeExpression(false /*mayOmitBraces*/)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseBracketNameInPropertyAndParamTag() (name *ast.EntityName, isBracketed bool) {
|
|
// Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar'
|
|
isBracketed = p.parseOptionalJsdoc(ast.KindOpenBracketToken)
|
|
if isBracketed {
|
|
p.skipWhitespace()
|
|
}
|
|
// a markdown-quoted name: `arg` is not legal jsdoc, but occurs in the wild
|
|
isBackquoted := p.parseOptionalJsdoc(ast.KindBacktickToken)
|
|
name = p.parseJSDocEntityName()
|
|
if isBackquoted {
|
|
p.parseExpectedTokenJSDoc(ast.KindBacktickToken)
|
|
}
|
|
if isBracketed {
|
|
p.skipWhitespace()
|
|
// May have an optional default, e.g. '[foo = 42]'
|
|
if p.parseOptionalToken(ast.KindEqualsToken) != nil {
|
|
p.parseExpression()
|
|
}
|
|
|
|
p.parseExpected(ast.KindCloseBracketToken)
|
|
}
|
|
|
|
return name, isBracketed
|
|
}
|
|
|
|
func isObjectOrObjectArrayTypeReference(node *ast.TypeNode) bool {
|
|
switch node.Kind {
|
|
case ast.KindObjectKeyword:
|
|
return true
|
|
case ast.KindArrayType:
|
|
return isObjectOrObjectArrayTypeReference(node.AsArrayTypeNode().ElementType)
|
|
default:
|
|
if ast.IsTypeReferenceNode(node) {
|
|
ref := node.AsTypeReferenceNode()
|
|
return ast.IsIdentifier(ref.TypeName) && ref.TypeName.AsIdentifier().Text == "Object" && ref.TypeArguments == nil
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseParameterOrPropertyTag(start int, tagName *ast.IdentifierNode, target propertyLikeParse, indent int) *ast.Node {
|
|
typeExpression := p.tryParseTypeExpression()
|
|
isNameFirst := typeExpression == nil
|
|
p.skipWhitespaceOrAsterisk()
|
|
|
|
name, isBracketed := p.parseBracketNameInPropertyAndParamTag()
|
|
indentText := p.skipWhitespaceOrAsterisk()
|
|
|
|
if isNameFirst && p.lookAhead(func(p *Parser) bool { _, ok := p.parseJSDocLinkPrefix(); return !ok }) {
|
|
typeExpression = p.tryParseTypeExpression()
|
|
}
|
|
|
|
comment := p.parseTrailingTagComments(start, p.nodePos(), indent, indentText)
|
|
|
|
nestedTypeLiteral := p.parseNestedTypeLiteral(typeExpression, name, target, indent)
|
|
if nestedTypeLiteral != nil {
|
|
typeExpression = nestedTypeLiteral
|
|
isNameFirst = true
|
|
}
|
|
var result *ast.Node /* JSDocPropertyTag | JSDocParameterTag */
|
|
if target == propertyLikeParseProperty {
|
|
result = p.factory.NewJSDocPropertyTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment)
|
|
} else {
|
|
result = p.factory.NewJSDocParameterTag(tagName, name, isBracketed, typeExpression, isNameFirst, comment)
|
|
}
|
|
return p.finishNode(result, start)
|
|
}
|
|
|
|
func (p *Parser) parseNestedTypeLiteral(typeExpression *ast.Node, name *ast.EntityName, target propertyLikeParse, indent int) *ast.Node {
|
|
if typeExpression != nil && isObjectOrObjectArrayTypeReference(typeExpression.Type()) {
|
|
pos := p.nodePos()
|
|
var children []*ast.Node
|
|
for {
|
|
state := p.mark()
|
|
child := p.parseChildParameterOrPropertyTag(target, indent, name)
|
|
if child == nil {
|
|
p.rewind(state)
|
|
break
|
|
}
|
|
if child.Kind == ast.KindJSDocParameterTag || child.Kind == ast.KindJSDocPropertyTag {
|
|
children = append(children, child)
|
|
} else if child.Kind == ast.KindJSDocTemplateTag {
|
|
p.parseErrorAtRange(child.AsJSDocTemplateTag().TagName.Loc, diagnostics.A_JSDoc_template_tag_may_not_follow_a_typedef_callback_or_overload_tag)
|
|
}
|
|
}
|
|
if children != nil {
|
|
literal := p.finishNode(p.factory.NewJSDocTypeLiteral(children, typeExpression.Type().Kind == ast.KindArrayType), pos)
|
|
return p.finishNode(p.factory.NewJSDocTypeExpression(literal), pos)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) parseReturnTag(previousTags []*ast.Node, start int, tagName *ast.IdentifierNode, indent int, indentText string) *ast.Node {
|
|
if core.Some(previousTags, ast.IsJSDocReturnTag) {
|
|
p.parseErrorAt(tagName.Pos(), p.scanner.TokenStart(), diagnostics.X_0_tag_already_specified, tagName.Text())
|
|
}
|
|
|
|
typeExpression := p.tryParseTypeExpression()
|
|
return p.finishNode(p.factory.NewJSDocReturnTag(tagName, typeExpression, p.parseTrailingTagComments(start, p.nodePos(), indent, indentText)), start)
|
|
}
|
|
|
|
// pass indent=-1 to skip parsing trailing comments (as when a type tag is nested in a typedef)
|
|
func (p *Parser) parseTypeTag(previousTags []*ast.Node, start int, tagName *ast.IdentifierNode, indent int, indentText string) *ast.Node {
|
|
if core.Some(previousTags, ast.IsJSDocTypeTag) {
|
|
p.parseErrorAt(tagName.Pos(), p.scanner.TokenStart(), diagnostics.X_0_tag_already_specified, tagName.Text())
|
|
}
|
|
|
|
typeExpression := p.parseJSDocTypeExpression(true)
|
|
var comments *ast.NodeList
|
|
if indent != -1 {
|
|
comments = p.parseTrailingTagComments(start, p.nodePos(), indent, indentText)
|
|
}
|
|
return p.finishNode(p.factory.NewJSDocTypeTag(tagName, typeExpression, comments), start)
|
|
}
|
|
|
|
func (p *Parser) parseSeeTag(start int, tagName *ast.IdentifierNode, indent int, indentText string) *ast.Node {
|
|
isMarkdownOrJSDocLink := p.token == ast.KindOpenBracketToken || p.lookAhead(func(p *Parser) bool {
|
|
return p.nextTokenJSDoc() == ast.KindAtToken && tokenIsIdentifierOrKeyword(p.nextTokenJSDoc()) && isJSDocLinkTag(p.scanner.TokenValue())
|
|
})
|
|
var nameExpression *ast.Node
|
|
if !isMarkdownOrJSDocLink {
|
|
nameExpression = p.parseJSDocNameReference()
|
|
}
|
|
comments := p.parseTrailingTagComments(start, p.nodePos(), indent, indentText)
|
|
return p.finishNode(p.factory.NewJSDocSeeTag(tagName, nameExpression, comments), start)
|
|
}
|
|
|
|
func (p *Parser) parseImplementsTag(start int, tagName *ast.IdentifierNode, margin int, indentText string) *ast.Node {
|
|
className := p.parseExpressionWithTypeArgumentsForAugments()
|
|
return p.finishNode(p.factory.NewJSDocImplementsTag(tagName, className, p.parseTrailingTagComments(start, p.nodePos(), margin, indentText)), start)
|
|
}
|
|
|
|
func (p *Parser) parseAugmentsTag(start int, tagName *ast.IdentifierNode, margin int, indentText string) *ast.Node {
|
|
className := p.parseExpressionWithTypeArgumentsForAugments()
|
|
return p.finishNode(p.factory.NewJSDocAugmentsTag(tagName, className, p.parseTrailingTagComments(start, p.nodePos(), margin, indentText)), start)
|
|
}
|
|
|
|
func (p *Parser) parseSatisfiesTag(start int, tagName *ast.IdentifierNode, margin int, indentText string) *ast.Node {
|
|
typeExpression := p.parseJSDocTypeExpression(false)
|
|
comments := p.parseTrailingTagComments(start, p.nodePos(), margin, indentText)
|
|
return p.finishNode(p.factory.NewJSDocSatisfiesTag(tagName, typeExpression, comments), start)
|
|
}
|
|
|
|
func (p *Parser) parseImportTag(start int, tagName *ast.IdentifierNode, margin int, indentText string) *ast.Node {
|
|
afterImportTagPos := p.scanner.TokenFullStart()
|
|
|
|
var identifier *ast.IdentifierNode
|
|
if p.isIdentifier() {
|
|
identifier = p.parseIdentifier()
|
|
}
|
|
|
|
importClause := p.tryParseImportClause(identifier, afterImportTagPos, ast.KindTypeKeyword, true /*skipJSDocLeadingAsterisks*/)
|
|
moduleSpecifier := p.parseModuleSpecifier()
|
|
attributes := p.tryParseImportAttributes()
|
|
|
|
comments := p.parseTrailingTagComments(start, p.nodePos(), margin, indentText)
|
|
return p.finishNode(p.factory.NewJSDocImportTag(tagName, importClause, moduleSpecifier, attributes, comments), start)
|
|
}
|
|
|
|
func (p *Parser) parseExpressionWithTypeArgumentsForAugments() *ast.Node {
|
|
usedBrace := p.parseOptional(ast.KindOpenBraceToken)
|
|
pos := p.nodePos()
|
|
expression := p.parsePropertyAccessEntityNameExpression()
|
|
p.scanner.SetSkipJSDocLeadingAsterisks(true)
|
|
typeArguments := p.parseTypeArguments()
|
|
p.scanner.SetSkipJSDocLeadingAsterisks(false)
|
|
node := p.finishNode(p.factory.NewExpressionWithTypeArguments(expression, typeArguments), pos)
|
|
if usedBrace {
|
|
p.skipWhitespace()
|
|
p.parseExpected(ast.KindCloseBraceToken)
|
|
}
|
|
return node
|
|
}
|
|
|
|
func (p *Parser) parsePropertyAccessEntityNameExpression() *ast.Node {
|
|
pos := p.nodePos()
|
|
node := p.parseJSDocIdentifierName(nil)
|
|
for p.parseOptional(ast.KindDotToken) {
|
|
name := p.parseJSDocIdentifierName(nil)
|
|
node = p.finishNode(p.factory.NewPropertyAccessExpression(node, nil, name, ast.NodeFlagsNone), pos)
|
|
}
|
|
return node
|
|
}
|
|
|
|
func (p *Parser) parseSimpleTag(start int, createTag func(tagName *ast.IdentifierNode, comment *ast.NodeList) *ast.Node, tagName *ast.IdentifierNode, margin int, indentText string) *ast.Node {
|
|
return p.finishNode(createTag(tagName, p.parseTrailingTagComments(start, p.nodePos(), margin, indentText)), start)
|
|
}
|
|
|
|
func (p *Parser) parseThisTag(start int, tagName *ast.IdentifierNode, margin int, indentText string) *ast.Node {
|
|
typeExpression := p.parseJSDocTypeExpression(true)
|
|
p.skipWhitespace()
|
|
result := p.factory.NewJSDocThisTag(tagName, typeExpression, p.parseTrailingTagComments(start, p.nodePos(), margin, indentText))
|
|
return p.finishNode(result, start)
|
|
}
|
|
|
|
func (p *Parser) parseTypedefTag(start int, tagName *ast.IdentifierNode, indent int, indentText string) *ast.Node {
|
|
typeExpression := p.tryParseTypeExpression()
|
|
p.skipWhitespaceOrAsterisk()
|
|
fullName := p.parseJSDocIdentifierName(nil)
|
|
p.skipWhitespace()
|
|
comment := p.parseTagComments(indent, nil)
|
|
|
|
end := -1
|
|
hasChildren := false
|
|
if typeExpression == nil || isObjectOrObjectArrayTypeReference(typeExpression.Type()) {
|
|
var child *ast.Node
|
|
var childTypeTag *ast.JSDocTypeTag
|
|
var jsdocPropertyTags []*ast.Node
|
|
for {
|
|
state := p.mark()
|
|
child = p.parseChildPropertyTag(indent)
|
|
if child == nil {
|
|
p.rewind(state)
|
|
break
|
|
}
|
|
if child.Kind == ast.KindJSDocTemplateTag {
|
|
break
|
|
}
|
|
hasChildren = true
|
|
if child.Kind == ast.KindJSDocTypeTag {
|
|
if childTypeTag == nil {
|
|
childTypeTag = child.AsJSDocTypeTag()
|
|
} else {
|
|
lastError := p.parseErrorAtCurrentToken(diagnostics.A_JSDoc_typedef_comment_may_not_contain_multiple_type_tags)
|
|
if lastError != nil {
|
|
related := ast.NewDiagnostic(nil, core.NewTextRange(0, 0), diagnostics.The_tag_was_first_specified_here)
|
|
lastError.AddRelatedInfo(related)
|
|
}
|
|
break
|
|
}
|
|
} else {
|
|
jsdocPropertyTags = append(jsdocPropertyTags, child)
|
|
}
|
|
}
|
|
if hasChildren {
|
|
isArrayType := typeExpression != nil && typeExpression.Type().Kind == ast.KindArrayType
|
|
jsdocTypeLiteral := p.factory.NewJSDocTypeLiteral(jsdocPropertyTags, isArrayType)
|
|
if childTypeTag != nil && childTypeTag.TypeExpression != nil && !isObjectOrObjectArrayTypeReference(childTypeTag.TypeExpression.Type()) {
|
|
typeExpression = childTypeTag.TypeExpression
|
|
} else {
|
|
typeExpression = p.finishNode(jsdocTypeLiteral, jsdocPropertyTags[0].Pos())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only include the characters between the name end and the next token if a comment was actually parsed out - otherwise it's just whitespace
|
|
if end == -1 {
|
|
if hasChildren && typeExpression != nil {
|
|
end = typeExpression.End()
|
|
} else if comment != nil {
|
|
end = p.nodePos()
|
|
} else if fullName != nil {
|
|
end = fullName.End()
|
|
} else if typeExpression != nil {
|
|
end = typeExpression.End()
|
|
} else {
|
|
end = tagName.End()
|
|
}
|
|
}
|
|
|
|
if comment == nil {
|
|
comment = p.parseTrailingTagComments(start, end, indent, indentText)
|
|
}
|
|
|
|
typedefTag := p.finishNodeWithEnd(p.factory.NewJSDocTypedefTag(tagName, typeExpression, fullName, comment), start, end)
|
|
if typeExpression != nil {
|
|
typeExpression.Parent = typedefTag // forcibly overwrite parent potentially set by inner type expression parse
|
|
}
|
|
return typedefTag
|
|
}
|
|
|
|
func (p *Parser) parseCallbackTagParameters(indent int) *ast.NodeList {
|
|
var child *ast.Node
|
|
var parameters []*ast.Node
|
|
pos := p.nodePos()
|
|
for {
|
|
state := p.mark()
|
|
child = p.parseChildParameterOrPropertyTag(propertyLikeParseCallbackParameter, indent, nil)
|
|
if child == nil {
|
|
p.rewind(state)
|
|
break
|
|
}
|
|
if child.Kind == ast.KindJSDocTemplateTag {
|
|
p.parseErrorAtRange(child.AsJSDocTemplateTag().TagName.Loc, diagnostics.A_JSDoc_template_tag_may_not_follow_a_typedef_callback_or_overload_tag)
|
|
break
|
|
}
|
|
parameters = append(parameters, child)
|
|
}
|
|
return p.newNodeList(core.NewTextRange(pos, p.nodePos()), parameters)
|
|
}
|
|
|
|
func (p *Parser) parseJSDocSignature(start int, indent int) *ast.Node {
|
|
parameters := p.parseCallbackTagParameters(indent)
|
|
var returnTag *ast.JSDocTag
|
|
state := p.mark()
|
|
if p.parseOptionalJsdoc(ast.KindAtToken) {
|
|
tag := p.parseTag(nil, indent)
|
|
if tag.Kind == ast.KindJSDocReturnTag {
|
|
returnTag = tag
|
|
}
|
|
}
|
|
if returnTag == nil {
|
|
p.rewind(state)
|
|
}
|
|
return p.finishNode(p.factory.NewJSDocSignature(nil, parameters, returnTag), start)
|
|
}
|
|
|
|
func (p *Parser) parseCallbackTag(start int, tagName *ast.IdentifierNode, indent int, indentText string) *ast.Node {
|
|
fullName := p.parseJSDocIdentifierName(nil)
|
|
p.skipWhitespace()
|
|
comment := p.parseTagComments(indent, nil)
|
|
typeExpression := p.parseJSDocSignature(p.nodePos(), indent)
|
|
if comment == nil {
|
|
comment = p.parseTrailingTagComments(start, p.nodePos(), indent, indentText)
|
|
}
|
|
var end int
|
|
if comment != nil {
|
|
end = p.nodePos()
|
|
} else {
|
|
end = typeExpression.End()
|
|
}
|
|
return p.finishNodeWithEnd(p.factory.NewJSDocCallbackTag(tagName, typeExpression, fullName, comment), start, end)
|
|
}
|
|
|
|
func (p *Parser) parseOverloadTag(start int, tagName *ast.IdentifierNode, indent int, indentText string) *ast.Node {
|
|
p.skipWhitespace()
|
|
comment := p.parseTagComments(indent, nil)
|
|
typeExpression := p.parseJSDocSignature(start, indent)
|
|
if comment == nil {
|
|
comment = p.parseTrailingTagComments(start, p.nodePos(), indent, indentText)
|
|
}
|
|
var end int
|
|
if comment != nil {
|
|
end = p.nodePos()
|
|
} else {
|
|
end = typeExpression.End()
|
|
}
|
|
return p.finishNodeWithEnd(p.factory.NewJSDocOverloadTag(tagName, typeExpression, comment), start, end)
|
|
}
|
|
|
|
func textsEqual(a *ast.EntityName, b *ast.EntityName) bool {
|
|
for !ast.IsIdentifier(a) || !ast.IsIdentifier(b) {
|
|
if !ast.IsIdentifier(a) && !ast.IsIdentifier(b) && a.AsQualifiedName().Right.Text() == b.AsQualifiedName().Right.Text() {
|
|
a = a.AsQualifiedName().Left
|
|
b = b.AsQualifiedName().Left
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
return a.AsIdentifier().Text == b.AsIdentifier().Text
|
|
}
|
|
|
|
func (p *Parser) parseChildPropertyTag(indent int) *ast.Node {
|
|
return p.parseChildParameterOrPropertyTag(propertyLikeParseProperty, indent, nil)
|
|
}
|
|
|
|
func (p *Parser) parseChildParameterOrPropertyTag(target propertyLikeParse, indent int, name *ast.EntityName) *ast.Node {
|
|
canParseTag := true
|
|
seenAsterisk := false
|
|
for {
|
|
switch p.nextTokenJSDoc() {
|
|
case ast.KindAtToken:
|
|
if canParseTag {
|
|
child := p.tryParseChildTag(target, indent)
|
|
if child != nil && name != nil &&
|
|
(child.Kind == ast.KindJSDocParameterTag || child.Kind == ast.KindJSDocPropertyTag) &&
|
|
(ast.IsIdentifier(child.Name()) || !textsEqual(name, child.Name().AsQualifiedName().Left)) {
|
|
return nil
|
|
}
|
|
return child
|
|
}
|
|
seenAsterisk = false
|
|
case ast.KindNewLineTrivia:
|
|
canParseTag = true
|
|
seenAsterisk = false
|
|
case ast.KindAsteriskToken:
|
|
if seenAsterisk {
|
|
canParseTag = false
|
|
}
|
|
seenAsterisk = true
|
|
case ast.KindIdentifier:
|
|
canParseTag = false
|
|
case ast.KindEndOfFile:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Parser) tryParseChildTag(target propertyLikeParse, indent int) *ast.Node {
|
|
if p.token != ast.KindAtToken {
|
|
panic("should only be called when at @")
|
|
}
|
|
start := p.scanner.TokenFullStart()
|
|
p.nextTokenJSDoc()
|
|
|
|
tagName := p.parseJSDocIdentifierName(nil)
|
|
indentText := p.skipWhitespaceOrAsterisk()
|
|
var t propertyLikeParse
|
|
switch tagName.Text() {
|
|
case "type":
|
|
if target == propertyLikeParseProperty {
|
|
return p.parseTypeTag(nil, start, tagName, -1, "")
|
|
}
|
|
case "prop", "property":
|
|
t = propertyLikeParseProperty
|
|
case "arg", "argument", "param":
|
|
t = propertyLikeParseParameter | propertyLikeParseCallbackParameter
|
|
case "template":
|
|
return p.parseTemplateTag(start, tagName, indent, indentText)
|
|
case "this":
|
|
return p.parseThisTag(start, tagName, indent, indentText)
|
|
default:
|
|
return nil
|
|
}
|
|
if (target & t) == 0 {
|
|
return nil
|
|
}
|
|
return p.parseParameterOrPropertyTag(start, tagName, target, indent)
|
|
}
|
|
|
|
func (p *Parser) parseTemplateTagTypeParameter() *ast.Node {
|
|
typeParameterPos := p.nodePos()
|
|
isBracketed := p.parseOptionalJsdoc(ast.KindOpenBracketToken)
|
|
if isBracketed {
|
|
p.skipWhitespace()
|
|
}
|
|
|
|
modifiers := p.parseModifiersEx(false, true /*permitConstAsModifier*/, false)
|
|
name := p.parseJSDocIdentifierName(diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces)
|
|
var defaultType *ast.Node
|
|
if isBracketed {
|
|
p.skipWhitespace()
|
|
p.parseExpected(ast.KindEqualsToken)
|
|
saveContextFlags := p.contextFlags
|
|
p.setContextFlags(ast.NodeFlagsJSDoc, true)
|
|
defaultType = p.parseJSDocType()
|
|
p.contextFlags = saveContextFlags
|
|
p.parseExpected(ast.KindCloseBracketToken)
|
|
}
|
|
|
|
if ast.NodeIsMissing(name) {
|
|
return nil
|
|
}
|
|
return p.finishNode(p.factory.NewTypeParameterDeclaration(modifiers, name, nil /*constraint*/, defaultType), typeParameterPos)
|
|
}
|
|
|
|
func (p *Parser) parseTemplateTagTypeParameters() *ast.TypeParameterList {
|
|
typeParameters := ast.TypeParameterList{}
|
|
for ok := true; ok; ok = p.parseOptionalJsdoc(ast.KindCommaToken) { // do-while loop
|
|
p.skipWhitespace()
|
|
node := p.parseTemplateTagTypeParameter()
|
|
if node != nil {
|
|
typeParameters.Nodes = append(typeParameters.Nodes, node)
|
|
}
|
|
p.skipWhitespaceOrAsterisk()
|
|
}
|
|
return &typeParameters
|
|
}
|
|
|
|
func (p *Parser) parseTemplateTag(start int, tagName *ast.IdentifierNode, indent int, indentText string) *ast.Node {
|
|
// The template tag looks like one of the following:
|
|
// @template T,U,V
|
|
// @template {Constraint} T
|
|
//
|
|
// According to the [closure docs](https://github.com/google/closure-compiler/wiki/Generic-Types#multiple-bounded-template-types):
|
|
// > Multiple bounded generics cannot be declared on the same line. For the sake of clarity, if multiple templates share the same
|
|
// > type bound they must be declared on separate lines.
|
|
//
|
|
// TODO: Determine whether we should enforce this in the checker.
|
|
// TODO: Consider moving the `constraint` to the first type parameter as we could then remove `getEffectiveConstraintOfTypeParameter`.
|
|
// TODO: Consider only parsing a single type parameter if there is a constraint.
|
|
var constraint *ast.Node
|
|
if p.token == ast.KindOpenBraceToken {
|
|
constraint = p.parseJSDocTypeExpression(false)
|
|
}
|
|
typeParameters := p.parseTemplateTagTypeParameters()
|
|
result := p.factory.NewJSDocTemplateTag(tagName, constraint, typeParameters, p.parseTrailingTagComments(start, p.nodePos(), indent, indentText))
|
|
return p.finishNode(result, start)
|
|
}
|
|
|
|
func (p *Parser) parseOptionalJsdoc(t ast.Kind) bool {
|
|
if p.token == t {
|
|
p.nextTokenJSDoc()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Parser) parseJSDocEntityName() *ast.EntityName {
|
|
var entity *ast.EntityName = p.parseJSDocIdentifierName(nil)
|
|
if p.parseOptional(ast.KindOpenBracketToken) {
|
|
p.parseExpected(ast.KindCloseBracketToken)
|
|
// Note that y[] is accepted as an entity name, but the postfix brackets are not saved for checking.
|
|
// Technically usejsdoc.org requires them for specifying a property of a type equivalent to Array<{ x: ...}>
|
|
// but it's not worth it to enforce that restriction.
|
|
}
|
|
for p.parseOptional(ast.KindDotToken) {
|
|
name := p.parseJSDocIdentifierName(nil)
|
|
if p.parseOptional(ast.KindOpenBracketToken) {
|
|
p.parseExpected(ast.KindCloseBracketToken)
|
|
}
|
|
pos := entity.Pos()
|
|
entity = p.finishNode(p.factory.NewQualifiedName(entity, name), pos)
|
|
}
|
|
return entity
|
|
}
|
|
|
|
func (p *Parser) parseJSDocIdentifierName(diagnosticMessage *diagnostics.Message) *ast.IdentifierNode {
|
|
if !tokenIsIdentifierOrKeyword(p.token) {
|
|
if diagnosticMessage != nil {
|
|
p.parseErrorAtCurrentToken(diagnosticMessage)
|
|
} else if isReservedWord(p.token) {
|
|
p.parseErrorAtCurrentToken(diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here, p.scanner.TokenText())
|
|
} else {
|
|
p.parseErrorAtCurrentToken(diagnostics.Identifier_expected)
|
|
}
|
|
return p.finishNode(p.newIdentifier(""), p.nodePos())
|
|
}
|
|
pos := p.scanner.TokenStart()
|
|
end := p.scanner.TokenEnd()
|
|
text := p.scanner.TokenValue()
|
|
p.internIdentifier(text)
|
|
p.nextTokenJSDoc()
|
|
return p.finishNodeWithEnd(p.newIdentifier(text), pos, end)
|
|
}
|