Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tsc/internal/parser/reparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ func (p *Parser) reparseUnhosted(tag *ast.Node, parent *ast.Node, jsDoc *ast.Nod
p.addDeepCloneReparse(importTag.Attributes),
)
p.finishReparsedNode(importDeclaration, tag)
p.jsdocInfos = append(p.jsdocInfos, JSDocInfo{parent: importDeclaration, jsDocs: []*ast.Node{jsDoc}})
importDeclaration.Flags |= ast.NodeFlagsHasJSDoc
p.reparseList = append(p.reparseList, importDeclaration)
case ast.KindJSDocOverloadTag:
// Create overload signatures only for function, method, and constructor declarations outside object literals
Expand Down
48 changes: 48 additions & 0 deletions tsc/internal/printer/emitcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type EmitContext struct {
varScopeStack core.Stack[*varScope]
letScopeStack core.Stack[*varScope]
emitHelpers collections.OrderedSet[*EmitHelper]
// Comments owned by a node they do not textually precede, keyed by the comment's start position.
claimedComments map[int]*ast.Node
}

type environmentFlags int
Expand Down Expand Up @@ -523,6 +525,7 @@ type emitNodeFlags uint32
const (
hasCommentRange emitNodeFlags = 1 << iota
hasSourceMapRange
claimsComment
)

type SnippetKind int
Expand Down Expand Up @@ -619,6 +622,51 @@ func (c *EmitContext) AssignCommentRange(to *ast.Node, from *ast.Node) {
c.SetCommentRange(to, c.CommentRange(from))
}

// Gives the comment at loc to node, which need not be the node the comment textually precedes. A
// claimed comment is emitted by its owner and skipped by every other node whose leading comment scan
// runs across it, and the owner in turn emits no leading comment other than the one it claimed. This
// lets a declaration reparsed out of a JSDoc tag keep the comment that declared it instead of
// leaving it in front of the following statement. scanFrom is where the printer starts looking for
// leading comments and must sit before the line the comment starts on, since comment scans only
// collect after a line break.
//
// node is nil when the declaration the comment documents is elided from the output, which leaves the
// comment with no one to emit it - it documents something the output does not contain. A claim by an
// emitted node supersedes one made for an elided declaration from the same comment; otherwise the
// first claim wins.
func (c *EmitContext) ClaimComment(node *ast.Node, scanFrom int, loc core.TextRange) {
if owner, claimed := c.claimedComments[loc.Pos()]; claimed && (owner != nil || node == nil) {
return
}
if c.claimedComments == nil {
c.claimedComments = make(map[int]*ast.Node)
}
c.claimedComments[loc.Pos()] = node
if node == nil {
return
}
c.SetCommentRange(node, core.NewTextRange(scanFrom, loc.End()))
c.emitNodes.Get(node).flags |= claimsComment
}

// Reports whether node was given a comment to emit by ClaimComment.
func (c *EmitContext) ClaimsComment(node *ast.Node) bool {
if node == nil {
return false
}
emitNode := c.emitNodes.TryGet(node)
return emitNode != nil && emitNode.flags&claimsComment != 0
}

// Reports whether node emits the comment starting at pos as one of its leading comments. node is nil
// when comments are emitted outside of any node.
func (c *EmitContext) EmitsLeadingComment(node *ast.Node, pos int) bool {
if owner, claimed := c.claimedComments[pos]; claimed {
return owner != nil && owner == node
}
return !c.ClaimsComment(node)
}

// Gets the range to use for a node when emitting source maps.
func (c *EmitContext) SourceMapRange(node *ast.Node) core.TextRange {
if emitNode := c.emitNodes.TryGet(node); emitNode != nil && emitNode.flags&hasSourceMapRange != 0 {
Expand Down
33 changes: 29 additions & 4 deletions tsc/internal/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5435,7 +5435,7 @@ func (p *Printer) emitLeadingCommentsOfNode(node *ast.Node, emitFlags EmitFlags,
// Emit leading comments if the position is not synthesized and the node
// has not opted out from emitting leading comments.
if !skipLeadingComments {
p.emitLeadingComments(pos, node.Kind == ast.KindNotEmittedStatement /*elided*/)
p.emitLeadingCommentsWorker(pos, node, node.Kind == ast.KindNotEmittedStatement /*elided*/)
}

if !skipLeadingComments || (pos >= 0 && (emitFlags&EFNoLeadingComments) != 0) {
Expand Down Expand Up @@ -5533,6 +5533,12 @@ func (p *Printer) writeSynthesizedComment(comment SynthesizedComment) {
}

func (p *Printer) emitLeadingComments(pos int, elided bool) bool {
return p.emitLeadingCommentsWorker(pos, nil /*node*/, elided)
}

// node decides ownership of comments claimed by way of EmitContext.ClaimComment, and is nil when
// comments are emitted outside of any node.
func (p *Printer) emitLeadingCommentsWorker(pos int, node *ast.Node, elided bool) bool {
// Emit the leading comments only if the container's pos doesn't match because the container should take care of emitting these comments
if p.commentsDisabled || p.currentSourceFile == nil || ast.PositionIsSynthesized(pos) || pos == p.containerPos {
return false
Expand All @@ -5557,15 +5563,21 @@ func (p *Printer) emitLeadingComments(pos int, elided bool) bool {
return false
}

// skip detached comments
if p.detachedCommentsInfo.Len() > 0 {
// skip detached comments - a node claiming a comment has to keep scanning across them, since the
// comment it claims may be one the detached pass left behind for it, and the node the comments
// were detached from still needs to consume them
claimsComment := p.emitContext.ClaimsComment(node)
if p.detachedCommentsInfo.Len() > 0 && !claimsComment {
if info := p.detachedCommentsInfo.Peek(); info.nodePos == pos {
pos = p.detachedCommentsInfo.Pop().detachedCommentEndPos
}
}

var comments []ast.CommentRange
for comment := range scanner.GetLeadingCommentRanges(p.emitContext.Factory.AsNodeFactory(), p.currentSourceFile.Text(), pos) {
if !p.emitContext.EmitsLeadingComment(node, comment.Pos()) {
continue
}
if p.shouldWriteComment(comment) && p.shouldEmitCommentIfTripleSlash(comment, tripleSlash) {
comments = append(comments, comment)
}
Expand All @@ -5576,7 +5588,13 @@ func (p *Printer) emitLeadingComments(pos int, elided bool) bool {
}

// Leading comments are emitted as /*leading comment1*/space/*leading comment*/space
return p.emitComments(comments, commentSeparatorAfter)
hasWrittenComment := p.emitComments(comments, commentSeparatorAfter)
if hasWrittenComment && claimsComment && !p.writer.IsAtStartOfLine() {
// A claimed comment no longer precedes the text it did in the source, so it cannot borrow
// that text's line break.
p.writeLine()
}
return hasWrittenComment
}

func (p *Printer) shouldEmitCommentIfTripleSlash(comment ast.CommentRange, tripleSlash core.Tristate) bool {
Expand Down Expand Up @@ -5722,6 +5740,13 @@ func (p *Printer) emitDetachedComments(textRange core.TextRange) (result detache
}
}

if !p.emitContext.EmitsLeadingComment(nil /*node*/, comment.Pos()) {
// A claimed comment is emitted by its owner, which comes after this point in the
// output. Detaching the rest of the run would print it ahead of the owner and put
// the comments out of source order.
break
}

detachedComments = append(detachedComments, comment)
lastComment = comment
}
Expand Down
95 changes: 95 additions & 0 deletions tsc/internal/transformers/declarations/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,104 @@ func (tx *DeclarationTransformer) transformAndReplaceLatePaintedStatements(state
}
}

// Claim over the statements as they went in, not over `results`, so that a comment whose
// declaration was just elided is claimed by nothing rather than left for the next statement.
tx.claimReparsedJSDocComments(statements.Nodes)
return tx.Factory().NewNodeList(results)
}

// Tags that reparse into a top-level declaration the containing JSDoc comment can be claimed for.
func declaresOwnDeclaration(tag *ast.Node) bool {
switch tag.Kind {
case ast.KindJSDocTypedefTag, ast.KindJSDocCallbackTag, ast.KindJSDocImportTag:
return true
}
return false
}

// Tags whose meaning belongs to a node other than the declarations reparsed out of the containing
// comment: those the parser applies to the comment's host - see reparseHosted - plus `@overload`,
// which is emitted as a signature of its own. `@template` is the exception, since alongside a
// `@typedef` or `@callback` it declares type parameters of the type being declared and leaves the
// host none, per gatherTypeParameters.
func documentsAnotherNode(tag *ast.Node, declaresType bool) bool {
switch tag.Kind {
case ast.KindJSDocTypeTag, ast.KindJSDocSatisfiesTag, ast.KindJSDocParameterTag, ast.KindJSDocThisTag,
ast.KindJSDocReturnTag, ast.KindJSDocReadonlyTag, ast.KindJSDocPrivateTag, ast.KindJSDocPublicTag,
ast.KindJSDocProtectedTag, ast.KindJSDocOverrideTag, ast.KindJSDocImplementsTag,
ast.KindJSDocAugmentsTag, ast.KindJSDocOverloadTag:
return true
case ast.KindJSDocTemplateTag:
return !declaresType
}
return false
}

// A JSDoc comment documents the declarations reparsed out of it, rather than the statement it is
// attached to, when it produces such a declaration and carries nothing that documents anything else.
// Whatever remains - the description, `@see`, `@example` and the like - describes what the comment
// declares.
func documentsOnlyReparsedDeclarations(jsdoc *ast.Node) bool {
tags := jsdoc.AsJSDoc().Tags
if tags == nil || !core.Some(tags.Nodes, declaresOwnDeclaration) {
return false
}
declaresType := core.Some(tags.Nodes, func(tag *ast.Node) bool {
return ast.IsJSDocTypedefTag(tag) || ast.IsJSDocCallbackTag(tag)
})
return !core.Some(tags.Nodes, func(tag *ast.Node) bool { return documentsAnotherNode(tag, declaresType) })
}

// A JSDoc node starts at the full start of the node it documents rather than at its own `/**`, so
// recover the range of the comment itself.
func (tx *DeclarationTransformer) commentRangeOfJSDoc(jsdoc *ast.Node, file *ast.SourceFile) (core.TextRange, bool) {
for comment := range scanner.GetLeadingCommentRanges(tx.EmitContext().Factory.AsNodeFactory(), file.Text(), jsdoc.Pos()) {
if comment.End() == jsdoc.End() {
return comment.TextRange, true
}
}
return core.TextRange{}, false
}

// The node a statement takes in the output, or nil when it is elided.
func (tx *DeclarationTransformer) emittedForm(statement *ast.Node) *ast.Node {
if !ast.IsLateVisibilityPaintedStatement(statement) {
return statement
}
replacement, replaced := tx.lateStatementReplacementMap[ast.GetNodeId(tx.EmitContext().MostOriginal(statement))]
if !replaced {
return statement
}
if replacement != nil && replacement.Kind == ast.KindSyntaxList {
return core.FirstOrNil(replacement.AsSyntaxList().Children)
}
return replacement
}

// Declarations reparsed from JSDoc (`@typedef`, `@callback`, `@import`) take the text range of the
// tag they came from, which sits inside the comment. The printer therefore finds no leading comment
// for them and hands the comment to the following statement instead, documenting the wrong
// declaration. Claim each such comment for the first declaration reparsed out of it.
func (tx *DeclarationTransformer) claimReparsedJSDocComments(statements []*ast.Node) {
file := tx.state.currentSourceFile
if file == nil || !ast.IsSourceFileJS(file) {
return
}
for _, statement := range statements {
original := tx.EmitContext().MostOriginal(statement)
if original.Flags&ast.NodeFlagsReparsed == 0 {
continue
}
jsdoc := core.FirstOrNil(original.EagerJSDoc(file))
if jsdoc == nil || !documentsOnlyReparsedDeclarations(jsdoc) {
Comment thread
ekalinin marked this conversation as resolved.
continue
}
if loc, ok := tx.commentRangeOfJSDoc(jsdoc, file); ok {
tx.EmitContext().ClaimComment(tx.emittedForm(statement), jsdoc.Pos(), loc)
}
}
}

func (tx *DeclarationTransformer) getReferencedFiles(outputFilePath string) (results []*ast.FileReference) {
// Handle path rewrites for triple slash ref comments
for _, pair := range tx.rawReferencedFiles {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ let v1 = { x: "test" };


//// [a.d.ts]
/**
* @typedef {{x: string}} Foo
*/
declare const _exports: {
a: number;
b: string;
};
export = _exports;
/**
* @typedef {{x: string}} Foo
*/
export type Foo = {
x: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ let v1 = { x: "test" };


//// [a.d.ts]
export type Foo = {
x: string;
};
/**
* @typedef {{x: string}} Foo
*/
export type Foo = {
x: string;
};
export declare const x = 1;
//// [b.d.ts]
export {};
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ declare class Test {
export default Test;
//// [index.d.ts]
import Test from './test/Test.js';
export type Options = {
test?: typeof import("./Test.js").default;
};
/**
* @typedef {Object} Options
* @property {typeof import("./Test.js").default} [test]
*/
export type Options = {
test?: typeof import("./Test.js").default;
};
declare class X extends Test {
test: import("./Test.js").default | undefined;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ module.exports = function loader(options) { };


//// [index.d.ts]
/**
* @typedef Options
* @property {string} opt
*/
export = loader;
/**
* @param {Options} options
*/
declare function loader(options: Options): void;
/**
* @typedef Options
* @property {string} opt
*/
export type Options = {
opt: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ class C3 extends C1 {
type A = {
a: string;
};
type B = {
b: number;
};
/**
* @typedef B
* @property {number} b
*/
type B = {
b: number;
};
declare class C1 {
/**
* @type {A}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ function f1() {}


//// [x.d.ts]
type Foo = (x: string) => number;
/**
* @callback Foo
* @param {string} x
* @returns {number}
*/
type Foo = (x: string) => number;
declare function f1(): void;
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export function f1() {}


//// [x.d.ts]
export type Foo = (x: string) => number;
/**
* @callback Foo
* @param {string} x
* @returns {number}
*/
export type Foo = (x: string) => number;
export declare function f1(): void;
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ declare namespace ModuleGraphConnection {
const _exported: typeof T;
export { _exported as T };
}
export type T = typeof T;
/** @typedef {typeof T} T */
export type T = typeof T;
declare const T: unique symbol;
//// [repro.d.ts]
export = Repro;
/** @typedef {import('./local-lib/ModuleGraphConnection')} ImportedType */
/** @type {ImportedType} */
declare class Repro {
}
/** @typedef {import('./local-lib/ModuleGraphConnection')} ImportedType */
export type ImportedType = import('./local-lib/ModuleGraphConnection');
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@


//// [a.d.ts]
type T = ("a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n")[];
/**
* @typedef {("a"|"b"|"c"|
* "d"|"e"|"f"|"g"|
Expand All @@ -23,3 +22,4 @@ type T = ("a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l"
* "k"|"l"|
* "m"|"n")[]} T
*/
type T = ("a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n")[];
Loading