Skip to content

Commit

Permalink
fix #221: preserve certain statement-level comments
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 6, 2020
1 parent dd9fc67 commit d7679fc
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 20 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

This should now work correctly.

* Preserve certain statement-level comments ([#221](https://github.com/evanw/esbuild/issues/221))

Statement-level comments starting with `//!` or `/*!` or containing `@preserve` or `@license` are now preserved in the output. This matches the behavior of other JavaScript tools such as [Terser](https://github.com/terser/terser).

## 0.5.22

* JavaScript build API can now avoid writing to the file system ([#139](https://github.com/evanw/esbuild/issues/139) and [#220](https:/evanw/esbuild/issues/220))
Expand Down
5 changes: 5 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,10 @@ type SEmpty struct{}
// This is a stand-in for a TypeScript type declaration
type STypeScript struct{}

type SComment struct {
Text string
}

type SDebugger struct{}

type SDirective struct {
Expand Down Expand Up @@ -866,6 +870,7 @@ type SContinue struct {
}

func (*SBlock) isStmt() {}
func (*SComment) isStmt() {}
func (*SDebugger) isStmt() {}
func (*SDirective) isStmt() {}
func (*SEmpty) isStmt() {}
Expand Down
117 changes: 99 additions & 18 deletions internal/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ type json struct {
allowComments bool
}

type Comment struct {
Loc ast.Loc
Text string
}

type Lexer struct {
log logging.Log
source logging.Source
Expand All @@ -238,6 +243,7 @@ type Lexer struct {
Token T
HasNewlineBefore bool
HasPureCommentBefore bool
CommentsToPreserveBefore []Comment
codePoint rune
StringLiteral []uint16
Identifier string
Expand Down Expand Up @@ -857,6 +863,7 @@ func (lexer *Lexer) NextInsideJSXElement() {
func (lexer *Lexer) Next() {
lexer.HasNewlineBefore = lexer.end == 0
lexer.HasPureCommentBefore = false
lexer.CommentsToPreserveBefore = nil

for {
lexer.start = lexer.end
Expand Down Expand Up @@ -1144,9 +1151,7 @@ func (lexer *Lexer) Next() {
if lexer.json.parse && !lexer.json.allowComments {
lexer.addRangeError(lexer.Range(), "JSON does not support comments")
}
if isPureComment(lexer.source.Contents[lexer.start:lexer.end]) {
lexer.HasPureCommentBefore = true
}
lexer.scanCommentText()
continue

case '*':
Expand Down Expand Up @@ -1178,9 +1183,7 @@ func (lexer *Lexer) Next() {
if lexer.json.parse && !lexer.json.allowComments {
lexer.addRangeError(lexer.Range(), "JSON does not support comments")
}
if isPureComment(lexer.source.Contents[lexer.start:lexer.end]) {
lexer.HasPureCommentBefore = true
}
lexer.scanCommentText()
continue

default:
Expand Down Expand Up @@ -2215,22 +2218,100 @@ func (lexer *Lexer) addRangeError(r ast.Range, text string) {
}
}

func isPureComment(text string) bool {
// Scan for "@__PURE__" or "#__PURE__"
for {
index := strings.Index(text, "__PURE__")
if index == -1 {
break
func (lexer *Lexer) scanCommentText() {
text := lexer.source.Contents[lexer.start:lexer.end]
hasPreserveAnnotation := len(text) > 2 && text[2] == '!'

for i, n := 0, len(text); i < n; i++ {
switch text[i] {
case '#':
rest := text[i+1:]
if strings.HasPrefix(rest, "__PURE__") {
lexer.HasPureCommentBefore = true
}

case '@':
rest := text[i+1:]
if strings.HasPrefix(rest, "__PURE__") {
lexer.HasPureCommentBefore = true
} else if strings.HasPrefix(rest, "preserve") || strings.HasPrefix(rest, "license") {
hasPreserveAnnotation = true
}
}
}

if hasPreserveAnnotation {
if text[1] == '*' {
text = removeMultiLineCommentIndent(lexer.source.Contents[:lexer.start], text)
}
if index > 0 {
c := text[index-1]
if c == '@' || c == '#' {
return true

lexer.CommentsToPreserveBefore = append(lexer.CommentsToPreserveBefore, Comment{
Loc: ast.Loc{Start: int32(lexer.start)},
Text: text,
})
}
}

func removeMultiLineCommentIndent(prefix string, text string) string {
// Figure out the initial indent
indent := 0
seekBackwardToNewline:
for len(prefix) > 0 {
c, size := utf8.DecodeLastRuneInString(prefix)
switch c {
case '\r', '\n', '\u2028', '\u2029':
break seekBackwardToNewline
}
prefix = prefix[:len(prefix)-size]
indent++
}

// Split the comment into lines
var lines []string
start := 0
for i, c := range text {
switch c {
case '\r', '\n':
// Don't double-append for Windows style "\r\n" newlines
if start <= i {
lines = append(lines, text[start:i])
}

start = i + 1

// Ignore the second part of Windows style "\r\n" newlines
if c == '\r' && start < len(text) && text[start] == '\n' {
start++
}

case '\u2028', '\u2029':
lines = append(lines, text[start:i])
start = i + 3
}
}
lines = append(lines, text[start:])

// Find the minimum indent over all lines after the first line
for _, line := range lines[1:] {
lineIndent := 0
for _, c := range line {
if !IsWhitespace(c) {
break
}
lineIndent++
}
if indent > lineIndent {
indent = lineIndent
}
}

// Trim the indent off of all lines after the first line
for i, line := range lines {
if i > 0 {
lines[i] = line[indent:]
}
text = text[index+8:]
}
return false
return strings.Join(lines, "\n")
}

func StringToUTF16(text string) []uint16 {
Expand Down
19 changes: 17 additions & 2 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -4897,7 +4897,22 @@ func (p *parser) parseStmtsUpTo(end lexer.T, opts parseStmtOpts) []ast.Stmt {
returnWithoutSemicolonStart := int32(-1)
opts.allowLexicalDecl = true

for p.lexer.Token != end {
for {
// Preserve some statement-level comments
comments := p.lexer.CommentsToPreserveBefore
if len(comments) > 0 {
for _, comment := range comments {
stmts = append(stmts, ast.Stmt{
Loc: comment.Loc,
Data: &ast.SComment{Text: comment.Text},
})
}
}

if p.lexer.Token == end {
break
}

stmt := p.parseStmt(opts)

// Skip TypeScript types entirely
Expand Down Expand Up @@ -5607,7 +5622,7 @@ func (p *parser) mangleIf(loc ast.Loc, s *ast.SIf, isTestBooleanConstant bool, t

func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt {
switch s := stmt.Data.(type) {
case *ast.SDebugger, *ast.SEmpty, *ast.SDirective:
case *ast.SDebugger, *ast.SEmpty, *ast.SDirective, *ast.SComment:
// These don't contain anything to traverse

case *ast.STypeScript:
Expand Down
40 changes: 40 additions & 0 deletions internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,46 @@ func TestTrimCodeInDeadControlFlow(t *testing.T) {
expectPrintedMangle(t, "if (1) { a(); b() } else { var a; var b; }", "if (1)\n a(), b();\nelse\n var a, b;\n")
}

func TestPreservedComments(t *testing.T) {
expectPrinted(t, "//", "")
expectPrinted(t, "//preserve", "")
expectPrinted(t, "//@__PURE__", "")
expectPrinted(t, "//!", "//!\n")
expectPrinted(t, "//@license", "//@license\n")
expectPrinted(t, "//@preserve", "//@preserve\n")

expectPrinted(t, "/**/", "")
expectPrinted(t, "/*preserve*/", "")
expectPrinted(t, "/*@__PURE__*/", "")
expectPrinted(t, "/*!*/", "/*!*/\n")
expectPrinted(t, "/*@license*/", "/*@license*/\n")
expectPrinted(t, "/*@preserve*/", "/*@preserve*/\n")

expectPrinted(t, "foo() //! test", "foo();\n//! test\n")
expectPrinted(t, "//! test\nfoo()", "//! test\nfoo();\n")
expectPrinted(t, "if (1) //! test\nfoo()", "if (1)\n foo();\n")
expectPrinted(t, "if (1) {//! test\nfoo()}", "if (1) {\n //! test\n foo();\n}\n")
expectPrinted(t, "if (1) {foo() //! test\n}", "if (1) {\n foo();\n //! test\n}\n")

expectPrinted(t, " /*!\r * Re-indent test\r */", "/*!\n * Re-indent test\n */\n")
expectPrinted(t, " /*!\n * Re-indent test\n */", "/*!\n * Re-indent test\n */\n")
expectPrinted(t, " /*!\r\n * Re-indent test\r\n */", "/*!\n * Re-indent test\n */\n")
expectPrinted(t, " /*!\u2028 * Re-indent test\u2028 */", "/*!\n * Re-indent test\n */\n")
expectPrinted(t, " /*!\u2029 * Re-indent test\u2029 */", "/*!\n * Re-indent test\n */\n")

expectPrinted(t, "\t\t/*!\r\t\t * Re-indent test\r\t\t */", "/*!\n * Re-indent test\n */\n")
expectPrinted(t, "\t\t/*!\n\t\t * Re-indent test\n\t\t */", "/*!\n * Re-indent test\n */\n")
expectPrinted(t, "\t\t/*!\r\n\t\t * Re-indent test\r\n\t\t */", "/*!\n * Re-indent test\n */\n")
expectPrinted(t, "\t\t/*!\u2028\t\t * Re-indent test\u2028\t\t */", "/*!\n * Re-indent test\n */\n")
expectPrinted(t, "\t\t/*!\u2029\t\t * Re-indent test\u2029\t\t */", "/*!\n * Re-indent test\n */\n")

expectPrinted(t, "x\r /*!\r * Re-indent test\r */", "x;\n/*!\n * Re-indent test\n */\n")
expectPrinted(t, "x\n /*!\n * Re-indent test\n */", "x;\n/*!\n * Re-indent test\n */\n")
expectPrinted(t, "x\r\n /*!\r\n * Re-indent test\r\n */", "x;\n/*!\n * Re-indent test\n */\n")
expectPrinted(t, "x\u2028 /*!\u2028 * Re-indent test\u2028 */", "x;\n/*!\n * Re-indent test\n */\n")
expectPrinted(t, "x\u2029 /*!\u2029 * Re-indent test\u2029 */", "x;\n/*!\n * Re-indent test\n */\n")
}

func TestUnicodeWhitespace(t *testing.T) {
whitespace := []string{
"\u0009", // character tabulation
Expand Down
23 changes: 23 additions & 0 deletions internal/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2106,6 +2106,29 @@ func (p *printer) printStmt(stmt ast.Stmt) {
p.addSourceMapping(stmt.Loc)

switch s := stmt.Data.(type) {
case *ast.SComment:
text := s.Text
if strings.HasPrefix(text, "/*") {
// Re-indent multi-line comments
for {
newline := strings.IndexByte(text, '\n')
if newline == -1 {
break
}
p.printIndent()
p.print(text[:newline+1])
text = text[newline+1:]
}
p.printIndent()
p.print(text)
p.printNewline()
} else {
// Print a mandatory newline after single-line comments
p.printIndent()
p.print(text)
p.print("\n")
}

case *ast.SFunction:
p.printIndent()
p.printSpaceBeforeIdentifier()
Expand Down
4 changes: 4 additions & 0 deletions internal/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,4 +579,8 @@ func TestMinify(t *testing.T) {
expectPrintedMinify(t, "exports", "exports;")
expectPrintedMinify(t, "require", "require;")
expectPrintedMinify(t, "module", "module;")

// Comment statements must not affect their surroundings when minified
expectPrintedMinify(t, "//!single\nthrow 1 + 2", "//!single\nthrow 1+2;")
expectPrintedMinify(t, "/*!multi-\nline*/\nthrow 1 + 2", "/*!multi-\nline*/throw 1+2;")
}

0 comments on commit d7679fc

Please sign in to comment.