diff --git a/formatter/format_test.go b/formatter/format_test.go index 0b9e4c9..95fe4cc 100644 --- a/formatter/format_test.go +++ b/formatter/format_test.go @@ -67,6 +67,45 @@ func runCase(t *testing.T, src string, opts Options, want string) { } } +func TestFormatAnnotations(t *testing.T) { + tests := []struct { + name string + src string + want string + }{ + { + name: "annotation before a service is preserved", + src: "@naming.PreviouslyKnownAs{'namespace_': 'x'}\nservice Foo {\n void bar()\n}\n", + want: "@naming.PreviouslyKnownAs{'namespace_': 'x'}\nservice Foo {\n void bar()\n}\n", + }, + { + name: "empty annotation before an enum", + src: "@deprecation.Deprecated{}\nenum Status {\n A\n B\n}\n", + want: "@deprecation.Deprecated{}\nenum Status { A, B }\n", + }, + { + name: "multiple annotations keep order", + src: "@a.B{}\n@c.D{'k': 'v'}\nservice Foo {\n\n}\n", + want: "@a.B{}\n@c.D{'k': 'v'}\nservice Foo {\n\n}\n", + }, + { + name: "comment and annotation before a declaration", + src: "// keep me\n@naming.X{'a': 'b'}\nstruct S {}\n", + want: "// keep me\n@naming.X{'a': 'b'}\nstruct S {}\n", + }, + { + name: "annotation after a blank line keeps the blank line", + src: "struct A {}\n\n@naming.X{}\nservice B {\n\n}\n", + want: "struct A {}\n\n@naming.X{}\nservice B {\n\n}\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runCase(t, tt.src, testOpts(80), tt.want) + }) + } +} + func TestFormatHeaders(t *testing.T) { tests := []struct { name string diff --git a/syntax/lexer.go b/syntax/lexer.go index 6b0c09b..f8c853f 100644 --- a/syntax/lexer.go +++ b/syntax/lexer.go @@ -158,6 +158,7 @@ const ( TriviaLineComment TriviaKind = iota // // or # TriviaBlockComment // /* */ TriviaDocComment // /** */ + TriviaAnnotation // @name{...} to end of line ) func (k TriviaKind) String() string { @@ -168,6 +169,8 @@ func (k TriviaKind) String() string { return "block comment" case TriviaDocComment: return "doc comment" + case TriviaAnnotation: + return "annotation" } return fmt.Sprintf("TriviaKind(%d)", uint8(k)) } @@ -285,6 +288,11 @@ func (l *lexer) scanTrivia(prevLine int) (leading, trailing []Trivia, blankLines leading, trailing = l.appendComment(leading, trailing, prevLine, l.scanBlockComment()) case c == '#': leading, trailing = l.appendComment(leading, trailing, prevLine, l.scanLineComment()) + case c == '@': + // Java-style annotations (@name{...}) are preserved as trivia, + // like comments, so they round-trip without being part of the + // grammar. + leading, trailing = l.appendComment(leading, trailing, prevLine, l.scanLineAnnotation()) default: return leading, trailing, blankLines } @@ -337,6 +345,17 @@ func (l *lexer) scanLineComment() Trivia { return l.finishTrivia(TriviaLineComment, start) } +// scanLineAnnotation scans an @annotation line: from '@' to the end of the +// line, verbatim. Like line comments, the newline itself is left for the +// whitespace scanner. +func (l *lexer) scanLineAnnotation() Trivia { + start := l.pos() + for l.off < len(l.src) && l.src[l.off] != '\n' && l.src[l.off] != '\r' { + l.advanceRune() + } + return l.finishTrivia(TriviaAnnotation, start) +} + // scanBlockComment scans a /* */ or /** */ comment. /** ... */ yields a doc // comment trivia; everything else a block comment trivia. An unterminated // comment consumes the rest of the input and reports an error. diff --git a/syntax/lexer_test.go b/syntax/lexer_test.go index b405575..f3263ea 100644 --- a/syntax/lexer_test.go +++ b/syntax/lexer_test.go @@ -294,6 +294,20 @@ func TestLexTrivia(t *testing.T) { {idx: 1, trailing: []string{"/* a */", "// b"}}, }, }, + { + "annotation is leading trivia of the next declaration", + "@naming.PreviouslyKnownAs{'namespace_': 'x'}\nservice Foo {}", + []triviaCheck{ + {idx: 0, leading: []string{"@naming.PreviouslyKnownAs{'namespace_': 'x'}"}}, + }, + }, + { + "annotation inside a struct body attaches to the closing brace", + "struct S {\n 1: string x\n @weird\n}", + []triviaCheck{ + {idx: 7, leading: []string{"@weird"}}, + }, + }, { "doc comment kind", "/** doc */\nstruct S {}", @@ -404,6 +418,8 @@ func TestLexTriviaKinds(t *testing.T) { {"line", "// a\n# b\n", []TriviaKind{TriviaLineComment, TriviaLineComment}}, {"block and doc", "/** d */\n/* b */\n", []TriviaKind{TriviaDocComment, TriviaBlockComment}}, {"silly comment is doc", "/***/\n", []TriviaKind{TriviaDocComment}}, + {"annotation", "@naming.PreviouslyKnownAs{'x': 'y'}\n", []TriviaKind{TriviaAnnotation}}, + {"annotation with comment", "@deprecation.Deprecated{}\n// note\n", []TriviaKind{TriviaAnnotation, TriviaLineComment}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -483,7 +499,7 @@ func TestLexErrors(t *testing.T) { }, { name: "unexpected character with recovery", - src: "foo @ bar", + src: "foo $ bar", wantErrs: []string{"unexpected character"}, wantKinds: []TokenKind{TokenIdentifier, TokenIdentifier, TokenEOF}, }, diff --git a/tests/e2e/enums/test.sh b/tests/e2e/enums/test.sh index 943c657..5378606 100755 --- a/tests/e2e/enums/test.sh +++ b/tests/e2e/enums/test.sh @@ -13,7 +13,7 @@ do align=${options[1]} field_line_comma=${options[2]} echo "indent: ${indent}, align: ${align}, field_line_comma: ${field_line_comma}" - got=$(./bin/thriftls format -indent "${indent}" -align "${align}" -fieldLineComma "${field_line_comma}" tests/e2e/enums/enums.thrift) + got=$("$THRIFTLS_BIN" format -indent "${indent}" -align "${align}" -fieldLineComma "${field_line_comma}" tests/e2e/enums/enums.thrift) expected=$(cat "$f") if [ "$got" == "$expected" ];then echo "pass" diff --git a/tests/e2e/field_line_comma/test.sh b/tests/e2e/field_line_comma/test.sh index 2dad746..d8874ae 100755 --- a/tests/e2e/field_line_comma/test.sh +++ b/tests/e2e/field_line_comma/test.sh @@ -11,7 +11,7 @@ do read -ra options <<<"$substr" field_line_comma=${options[0]} echo "fieldLineComma: ${field_line_comma}" - got=$(./bin/thriftls format -fieldLineComma "${field_line_comma}" tests/e2e/field_line_comma/fields.thrift) + got=$("$THRIFTLS_BIN" format -fieldLineComma "${field_line_comma}" tests/e2e/field_line_comma/fields.thrift) expected=$(cat "$f") if [ "$got" == "$expected" ];then echo "pass" diff --git a/tests/e2e/fields/test.sh b/tests/e2e/fields/test.sh index dbda413..c07b2fd 100644 --- a/tests/e2e/fields/test.sh +++ b/tests/e2e/fields/test.sh @@ -12,7 +12,7 @@ do indent=${options[0]} align=${options[1]} echo "indent: ${indent}, align: ${align}" - got=$(./bin/thriftls format -indent "${indent}" -align "${align}" tests/e2e/fields/fields.thrift) + got=$("$THRIFTLS_BIN" format -indent "${indent}" -align "${align}" tests/e2e/fields/fields.thrift) expected=$(cat "$f") if [ "$got" == "$expected" ];then echo "pass" diff --git a/tests/e2e/run-e2e.sh b/tests/e2e/run-e2e.sh old mode 100644 new mode 100755 index dee808a..f24a114 --- a/tests/e2e/run-e2e.sh +++ b/tests/e2e/run-e2e.sh @@ -1,6 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +BIN_DIR=$(mktemp -d) +trap 'rm -rf "$BIN_DIR"' EXIT +go build -o "$BIN_DIR/thriftls" . +export THRIFTLS_BIN="$BIN_DIR/thriftls" + for f in ./tests/e2e/* do - if test -d "$f";then + if test -d "$f"; then bash "$f"/test.sh fi done