2025-10-15 10:12:44 +03:00

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