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