├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── comment.go ├── comment_test.go ├── edition.go ├── edition_test.go ├── enum.go ├── enum_test.go ├── extensions.go ├── extensions_test.go ├── field.go ├── field_test.go ├── go.mod ├── group.go ├── group_test.go ├── import.go ├── import_test.go ├── literals.go ├── literals_test.go ├── message.go ├── message_test.go ├── noop_visitor.go ├── oneof.go ├── oneof_test.go ├── option.go ├── option_test.go ├── package.go ├── package_test.go ├── parent_accessor.go ├── parent_test.go ├── parser.go ├── parser_test.go ├── proto.go ├── protobuf_test.go ├── range.go ├── range_test.go ├── reserved.go ├── reserved_test.go ├── service.go ├── service_test.go ├── syntax.go ├── syntax_test.go ├── token.go ├── token_test.go ├── visitor.go ├── visitor_test.go ├── walk.go └── walk_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: emicklei -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will test a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: 11 | - '*' # matches every branch that doesn't contain a '/' 12 | - '*/*' # matches every branch containing a single '/' 13 | - '**' # matches every branch 14 | - '!master' # excludes master 15 | 16 | jobs: 17 | 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: '1.23' 27 | 28 | - name: Test 29 | run: go test -v -cover ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.tmp/ 3 | /.vscode/ 4 | /bin/ 5 | debug.test 6 | .DS_Store 7 | coverage.txt 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.12.x 4 | script: 5 | - make 6 | after_success: 7 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## v1.14.1 (2025-04-29) 2 | 3 | - fix option name with brackets (ISSUE #148) 4 | 5 | ## v1.14.0 (2024-12-18) 6 | 7 | - parse edition element (PR #147, ISSUE #145) 8 | 9 | ## v1.13.4 (2024-12-17) 10 | 11 | - fixed handling identifiers known as numbers by scanner (PR #146) 12 | 13 | ## v1.13.3 (2024-12-04) 14 | 15 | - fixed inline comment in option (#143) 16 | 17 | ## v1.13.2 (2024-01-24) 18 | 19 | - allow keyword as field name (such as message,service, etc) 20 | 21 | ## v1.13.1 (2024-01-24) 22 | 23 | - allow embedded comment in between normal field parts (#131) 24 | 25 | ## v1.13.0 (2023-12-09) 26 | 27 | - walk options in Enum fields (#140) 28 | 29 | ## v1.12.2 (2023-11-02) 30 | 31 | - allow comments in array of literals of option (#138) 32 | - adds Comment field in Literal 33 | 34 | ## v1.12.1 (2023-07-18) 35 | 36 | - add IsDeprecated on EnumField 37 | 38 | ## v1.12.0 (2023-07-14) 39 | 40 | - add IsDeprecated on Field 41 | 42 | ## v1.11.2 (2023-05-01) 43 | 44 | - fix Parse failure on negative reserved enums (#133) 45 | 46 | ## v1.11.1 (2022-12-01) 47 | 48 | - added Doc for MapField so it implements Documented 49 | 50 | ## v1.11.0 51 | 52 | - added WithNormalField handler 53 | 54 | ## v1.10.0 55 | 56 | - added NoopVisitor and updated README with an example 57 | 58 | ## v1.9.2 59 | 60 | - fix for scanning content of single-quote option values (#129) 61 | 62 | ## v1.9.1 63 | 64 | - fix for issue #127 reserved keyword as suffix in type (#128) 65 | 66 | ## v1.9.0 67 | 68 | - Fix & guard Parent value for options (#124) 69 | 70 | ## v1.8.0 71 | 72 | - Add WithImport handler. 73 | 74 | ## v1.7.0 75 | 76 | - Add WithPackage handler for walking a proto. 77 | 78 | ## v1.6.17 79 | 80 | - add Oneof documented 81 | 82 | ## v1.6.16 83 | 84 | - Handle inline comments before definition body 85 | 86 | ## v1.6.15 87 | 88 | - Handle scanner change in Go 1.13 89 | 90 | ## v1.6.14 91 | 92 | - Handle comment inside option array value 93 | 94 | ## v1.6.13 95 | 96 | - fixes breaking change introduced by v1.6.11 w.r.t Literal 97 | 98 | ## < v1.6.12 99 | 100 | - see git log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ernest Micklei 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash -o pipefail 2 | UNAME_OS := $(shell uname -s) 3 | UNAME_ARCH := $(shell uname -m) 4 | 5 | TMP_BASE := .tmp 6 | TMP := $(TMP_BASE)/$(UNAME_OS)/$(UNAME_ARCH) 7 | TMP_BIN = $(TMP)/bin 8 | 9 | GOLINT_VERSION := 8f45f776aaf18cebc8d65861cc70c33c60471952 10 | GOLINT := $(TMP_BIN)/golint 11 | $(GOLINT): 12 | $(eval GOLINT_TMP := $(shell mktemp -d)) 13 | @cd $(GOLINT_TMP); go get github.com/golang/lint/golint@$(GOLINT_VERSION) 14 | @rm -rf $(GOLINT_TMP) 15 | 16 | ERRCHECK_VERSION := v1.2.0 17 | ERRCHECK := $(TMP_BIN)/errcheck 18 | $(ERRCHECK): 19 | $(eval ERRCHECK_TMP := $(shell mktemp -d)) 20 | @cd $(ERRCHECK_TMP); go get github.com/kisielk/errcheck@$(ERRCHECK_VERSION) 21 | @rm -rf $(ERRCHECK_TMP) 22 | 23 | STATICCHECK_VERSION := c2f93a96b099cbbec1de36336ab049ffa620e6d7 24 | STATICCHECK := $(TMP_BIN)/staticcheck 25 | $(STATICCHECK): 26 | $(eval STATICCHECK_TMP := $(shell mktemp -d)) 27 | @cd $(STATICCHECK_TMP); go get honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) 28 | @rm -rf $(STATICCHECK_TMP) 29 | 30 | unexport GOPATH 31 | export GO111MODULE := on 32 | export GOBIN := $(abspath $(TMP_BIN)) 33 | export PATH := $(GOBIN):$(PATH) 34 | 35 | .DEFAULT_GOAL := all 36 | 37 | .PHONY: all 38 | all: lint test 39 | 40 | .PHONY: install 41 | install: 42 | go install ./... 43 | 44 | .PHONY: golint 45 | golint: $(GOLINT) 46 | @# TODO: readd cmd/proto2gql when fixed 47 | @#for file in $(shell find . -name '*.go'); do 48 | for file in $(shell find . -name '*.go' | grep -v cmd/proto2gql); do \ 49 | golint $${file}; \ 50 | if [ -n "$$(golint $${file})" ]; then \ 51 | exit 1; \ 52 | fi; \ 53 | done 54 | 55 | .PHONY: vet 56 | vet: 57 | go vet ./... 58 | 59 | .PHONY: testdeps 60 | errcheck: $(ERRCHECK) 61 | errcheck ./... 62 | 63 | .PHONY: staticcheck 64 | staticcheck: $(STATICCHECK) 65 | staticcheck -checks "all -U1000" ./... 66 | 67 | .PHONY: lint 68 | # TODO: readd errcheck when fixed 69 | #lint: golint vet errcheck staticcheck 70 | #lint: golint vet staticcheck 71 | lint: golint vet 72 | 73 | .PHONY: test 74 | test: 75 | go test -race -coverprofile=coverage.txt -covermode=atomic ./... 76 | 77 | .PHONY: clean 78 | clean: 79 | go clean -i ./... 80 | 81 | .PHONY: integration 82 | integration: 83 | PB=y go test -cover 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proto 2 | 3 | [![Go](https://github.com/emicklei/proto/actions/workflows/go.yml/badge.svg)](https://github.com/emicklei/proto/actions/workflows/go.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/emicklei/proto)](https://goreportcard.com/report/github.com/emicklei/proto) 5 | [![GoDoc](https://pkg.go.dev/badge/github.com/emicklei/proto)](https://pkg.go.dev/github.com/emicklei/proto) 6 | [![codecov](https://codecov.io/gh/emicklei/proto/branch/master/graph/badge.svg)](https://codecov.io/gh/emicklei/proto) 7 | 8 | Package in Go for parsing Google Protocol Buffers [.proto files version 2 + 3, editions](https://developers.google.com/protocol-buffers/docs/reference/proto3-spec) 9 | 10 | ### install 11 | 12 | go get github.com/emicklei/proto 13 | 14 | ### usage 15 | 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/emicklei/proto" 23 | ) 24 | 25 | func main() { 26 | reader, _ := os.Open("test.proto") 27 | defer reader.Close() 28 | 29 | parser := proto.NewParser(reader) 30 | definition, _ := parser.Parse() 31 | 32 | proto.Walk(definition, 33 | proto.WithService(handleService), 34 | proto.WithMessage(handleMessage)) 35 | } 36 | 37 | func handleService(s *proto.Service) { 38 | fmt.Println(s.Name) 39 | } 40 | 41 | func handleMessage(m *proto.Message) { 42 | lister := new(optionLister) 43 | for _, each := range m.Elements { 44 | each.Accept(lister) 45 | } 46 | fmt.Println(m.Name) 47 | } 48 | 49 | type optionLister struct { 50 | proto.NoopVisitor 51 | } 52 | 53 | func (l optionLister) VisitOption(o *proto.Option) { 54 | fmt.Println(o.Name) 55 | } 56 | 57 | ### validation 58 | 59 | Current parser implementation is not completely validating `.proto` definitions. 60 | In many but not all cases, the parser will report syntax errors when reading unexpected charaters or tokens. 61 | Use some linting tools or `protoc` for full validation. 62 | 63 | ### contributions 64 | 65 | See [proto-contrib](https://github.com/emicklei/proto-contrib) for other contributions on top of this package such as protofmt, proto2xsd and proto2gql. 66 | [protobuf2map](https://github.com/emicklei/protobuf2map) is a small package for inspecting serialized protobuf messages using its `.proto` definition. 67 | 68 | © 2017-2025, [ernestmicklei.com](http://ernestmicklei.com). MIT License. Contributions welcome. 69 | -------------------------------------------------------------------------------- /comment.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "strings" 28 | "text/scanner" 29 | ) 30 | 31 | // Comment one or more comment text lines, either in c- or c++ style. 32 | type Comment struct { 33 | Position scanner.Position 34 | // Lines are comment text lines without prefixes //, ///, /* or suffix */ 35 | Lines []string 36 | Cstyle bool // refers to /* ... */, C++ style is using // 37 | ExtraSlash bool // is true if the comment starts with 3 slashes 38 | } 39 | 40 | // newComment returns a comment. 41 | func newComment(pos scanner.Position, lit string) *Comment { 42 | extraSlash := strings.HasPrefix(lit, "///") 43 | isCstyle := strings.HasPrefix(lit, "/*") && strings.HasSuffix(lit, "*/") 44 | var lines []string 45 | if isCstyle { 46 | withoutMarkers := strings.TrimRight(strings.TrimLeft(lit, "/*"), "*/") 47 | lines = strings.Split(withoutMarkers, "\n") 48 | } else { 49 | lines = strings.Split(strings.TrimLeft(lit, "/"), "\n") 50 | } 51 | return &Comment{Position: pos, Lines: lines, Cstyle: isCstyle, ExtraSlash: extraSlash} 52 | } 53 | 54 | type inlineComment struct { 55 | line string 56 | extraSlash bool 57 | } 58 | 59 | // Accept dispatches the call to the visitor. 60 | func (c *Comment) Accept(v Visitor) { 61 | v.VisitComment(c) 62 | } 63 | 64 | // Merge appends all lines from the argument comment. 65 | func (c *Comment) Merge(other *Comment) { 66 | c.Lines = append(c.Lines, other.Lines...) 67 | c.Cstyle = c.Cstyle || other.Cstyle 68 | } 69 | 70 | func (c Comment) hasTextOnLine(line int) bool { 71 | if len(c.Lines) == 0 { 72 | return false 73 | } 74 | return c.Position.Line <= line && line <= c.Position.Line+len(c.Lines)-1 75 | } 76 | 77 | // Message returns the first line or empty if no lines. 78 | func (c Comment) Message() string { 79 | if len(c.Lines) == 0 { 80 | return "" 81 | } 82 | return c.Lines[0] 83 | } 84 | 85 | // commentInliner is for types that can have an inline comment. 86 | type commentInliner interface { 87 | inlineComment(c *Comment) 88 | } 89 | 90 | // maybeScanInlineComment tries to scan comment on the current line ; if present then set it for the last element added. 91 | func maybeScanInlineComment(p *Parser, c elementContainer) { 92 | currentPos := p.scanner.Position 93 | // see if there is an inline Comment 94 | pos, tok, lit := p.next() 95 | esize := len(c.elements()) 96 | // seen comment and on same line and elements have been added 97 | if tCOMMENT == tok && pos.Line == currentPos.Line && esize > 0 { 98 | // if the last added element can have an inline comment then set it 99 | last := c.elements()[esize-1] 100 | if inliner, ok := last.(commentInliner); ok { 101 | // TODO skip multiline? 102 | inliner.inlineComment(newComment(pos, lit)) 103 | } 104 | } else { 105 | p.nextPut(pos, tok, lit) 106 | } 107 | } 108 | 109 | // takeLastCommentIfEndsOnLine removes and returns the last element of the list if it is a Comment 110 | func takeLastCommentIfEndsOnLine(list []Visitee, line int) (*Comment, []Visitee) { 111 | if len(list) == 0 { 112 | return nil, list 113 | } 114 | if last, ok := list[len(list)-1].(*Comment); ok && last.hasTextOnLine(line) { 115 | return last, list[:len(list)-1] 116 | } 117 | return nil, list 118 | } 119 | 120 | // mergeOrReturnComment creates a new comment and tries to merge it with the last element (if is a comment and is on the next line). 121 | func mergeOrReturnComment(elements []Visitee, lit string, pos scanner.Position) *Comment { 122 | com := newComment(pos, lit) 123 | esize := len(elements) 124 | if esize == 0 { 125 | return com 126 | } 127 | // last element must be a comment to merge 128 | last, ok := elements[esize-1].(*Comment) 129 | if !ok { 130 | return com 131 | } 132 | // do not merge c-style comments 133 | if last.Cstyle { 134 | return com 135 | } 136 | // last comment has text on previous line 137 | // TODO handle last line of file could be inline comment 138 | if !last.hasTextOnLine(pos.Line - 1) { 139 | return com 140 | } 141 | last.Merge(com) 142 | return nil 143 | } 144 | 145 | // parent is part of elementContainer 146 | func (c *Comment) parent(Visitee) {} 147 | 148 | // consumeCommentFor is for reading and taking all comment lines before the body of an element (starting at {) 149 | func consumeCommentFor(p *Parser, e elementContainer) { 150 | pos, tok, lit := p.next() 151 | if tok == tCOMMENT { 152 | if com := mergeOrReturnComment(e.elements(), lit, pos); com != nil { // not merged? 153 | e.addElement(com) 154 | } 155 | consumeCommentFor(p, e) // bit of recursion is fine 156 | } else { 157 | p.nextPut(pos, tok, lit) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /comment_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "testing" 28 | "text/scanner" 29 | ) 30 | 31 | var startPosition = scanner.Position{Line: 1, Column: 1} 32 | 33 | func TestCreateComment(t *testing.T) { 34 | c0 := newComment(startPosition, "") 35 | if got, want := len(c0.Lines), 1; got != want { 36 | t.Errorf("got [%v] want [%v]", got, want) 37 | } 38 | c1 := newComment(startPosition, `hello 39 | world`) 40 | if got, want := len(c1.Lines), 2; got != want { 41 | t.Errorf("got [%v] want [%v]", got, want) 42 | } 43 | if got, want := c1.Lines[0], "hello"; got != want { 44 | t.Errorf("got [%v] want [%v]", got, want) 45 | } 46 | if got, want := c1.Lines[1], "world"; got != want { 47 | t.Errorf("got [%v] want [%v]", got, want) 48 | } 49 | if got, want := c1.Cstyle, false; got != want { 50 | t.Errorf("got [%v] want [%v]", got, want) 51 | } 52 | } 53 | 54 | func TestTakeLastComment(t *testing.T) { 55 | c0 := newComment(startPosition, "hi") 56 | c1 := newComment(startPosition, "there") 57 | _, l := takeLastCommentIfEndsOnLine([]Visitee{c0, c1}, 1) 58 | if got, want := len(l), 1; got != want { 59 | t.Fatalf("got [%v] want [%v]", got, want) 60 | } 61 | if got, want := l[0], c0; got != want { 62 | t.Errorf("got [%v] want [%v]", c1, want) 63 | } 64 | } 65 | 66 | func TestParseCommentWithEmptyLinesIndentAndTripleSlash(t *testing.T) { 67 | proto := ` 68 | // comment 1 69 | // comment 2 70 | // 71 | // comment 3 72 | /// comment 4` 73 | p := newParserOn(proto) 74 | def, err := p.Parse() 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | //spew.Dump(def) 79 | if got, want := len(def.Elements), 1; got != want { 80 | t.Fatalf("got [%v] want [%v]", got, want) 81 | } 82 | 83 | if got, want := len(def.Elements[0].(*Comment).Lines), 5; got != want { 84 | t.Fatalf("got [%v] want [%v]", got, want) 85 | } 86 | if got, want := def.Elements[0].(*Comment).Lines[4], " comment 4"; got != want { 87 | t.Fatalf("got [%v] want [%v]", got, want) 88 | } 89 | if got, want := def.Elements[0].(*Comment).Position.Line, 2; got != want { 90 | t.Fatalf("got [%d] want [%d]", got, want) 91 | } 92 | if got, want := def.Elements[0].(*Comment).Cstyle, false; got != want { 93 | t.Fatalf("got [%v] want [%v]", got, want) 94 | } 95 | } 96 | 97 | func TestParseCStyleComment(t *testing.T) { 98 | proto := ` 99 | /*comment 1 100 | comment 2 101 | 102 | comment 3 103 | comment 4 104 | */` 105 | p := newParserOn(proto) 106 | def, err := p.Parse() 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | if got, want := len(def.Elements), 1; got != want { 111 | t.Fatalf("got [%v] want [%v]", got, want) 112 | } 113 | 114 | if got, want := len(def.Elements[0].(*Comment).Lines), 6; got != want { 115 | t.Fatalf("got [%v] want [%v]", got, want) 116 | } 117 | if got, want := def.Elements[0].(*Comment).Lines[3], "comment 3"; got != want { 118 | t.Fatalf("got [%v] want [%v]", got, want) 119 | } 120 | if got, want := def.Elements[0].(*Comment).Lines[4], " comment 4"; got != want { 121 | t.Fatalf("got [%v] want [%v]", got, want) 122 | } 123 | if got, want := def.Elements[0].(*Comment).Cstyle, true; got != want { 124 | t.Fatalf("got [%v] want [%v]", got, want) 125 | } 126 | } 127 | 128 | func TestParseCStyleCommentWithIndent(t *testing.T) { 129 | proto := ` 130 | /*comment 1 131 | comment 2 132 | 133 | comment 3 134 | comment 4 135 | */` 136 | p := newParserOn(proto) 137 | def, err := p.Parse() 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | if got, want := len(def.Elements), 1; got != want { 142 | t.Fatalf("got [%v] want [%v]", got, want) 143 | } 144 | 145 | if got, want := len(def.Elements[0].(*Comment).Lines), 6; got != want { 146 | t.Fatalf("got [%v] want [%v]", got, want) 147 | } 148 | if got, want := def.Elements[0].(*Comment).Lines[0], "comment 1"; got != want { 149 | t.Fatalf("got [%v] want [%v]", got, want) 150 | } 151 | if got, want := def.Elements[0].(*Comment).Lines[3], "\tcomment 3"; got != want { 152 | t.Fatalf("got [%v] want [%v]", got, want) 153 | } 154 | if got, want := def.Elements[0].(*Comment).Lines[4], "\t comment 4"; got != want { 155 | t.Fatalf("got [%v] want [%v]", got, want) 156 | } 157 | if got, want := def.Elements[0].(*Comment).Cstyle, true; got != want { 158 | t.Fatalf("got [%v] want [%v]", got, want) 159 | } 160 | } 161 | 162 | func TestParseCStyleOneLineComment(t *testing.T) { 163 | proto := `/* comment 1 */` 164 | p := newParserOn(proto) 165 | def, err := p.Parse() 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | if got, want := len(def.Elements), 1; got != want { 170 | t.Fatalf("got [%v] want [%v]", got, want) 171 | } 172 | 173 | if got, want := len(def.Elements[0].(*Comment).Lines), 1; got != want { 174 | t.Fatalf("got [%v] want [%v]", got, want) 175 | } 176 | if got, want := def.Elements[0].(*Comment).Lines[0], " comment 1 "; got != want { 177 | t.Errorf("got [%v] want [%v]", got, want) 178 | } 179 | if got, want := def.Elements[0].(*Comment).Cstyle, true; got != want { 180 | t.Errorf("got [%v] want [%v]", got, want) 181 | } 182 | } 183 | 184 | func TestParseCStyleInlineComment(t *testing.T) { 185 | proto := `message Foo { 186 | int64 hello = 1; /* 187 | comment 1 188 | */ 189 | }` 190 | p := newParserOn(proto) 191 | def := new(Proto) 192 | err := def.parse(p) 193 | if err != nil { 194 | t.Fatal(err) 195 | } 196 | m := def.Elements[0].(*Message) 197 | if len(m.Elements) != 1 { 198 | t.Fatal("expected one element", m.Elements) 199 | } 200 | f := m.Elements[0].(*NormalField) 201 | comment := f.InlineComment 202 | if comment == nil { 203 | t.Fatal("no inline comment") 204 | } 205 | if got, want := len(comment.Lines), 3; got != want { 206 | t.Fatalf("got [%v] want [%v]", got, want) 207 | } 208 | if got, want := comment.Lines[0], ""; got != want { 209 | t.Errorf("got [%v] want [%v]", got, want) 210 | } 211 | if got, want := comment.Lines[1], " comment 1"; got != want { 212 | t.Errorf("got [%v] want [%v]", got, want) 213 | } 214 | if got, want := comment.Cstyle, true; got != want { 215 | t.Errorf("got [%v] want [%v]", got, want) 216 | } 217 | } 218 | 219 | func TestParseCommentWithTripleSlash(t *testing.T) { 220 | proto := ` 221 | /// comment 1 222 | ` 223 | p := newParserOn(proto) 224 | def, err := p.Parse() 225 | if err != nil { 226 | t.Fatal(err) 227 | } 228 | if got, want := len(def.Elements), 1; got != want { 229 | t.Fatalf("got [%v] want [%v]", got, want) 230 | } 231 | if got, want := def.Elements[0].(*Comment).ExtraSlash, true; got != want { 232 | t.Fatalf("got [%v] want [%v]", got, want) 233 | } 234 | if got, want := def.Elements[0].(*Comment).Lines[0], " comment 1"; got != want { 235 | t.Fatalf("got [%v] want [%v]", got, want) 236 | } 237 | if got, want := def.Elements[0].(*Comment).Position.Line, 2; got != want { 238 | t.Fatalf("got [%d] want [%d]", got, want) 239 | } 240 | } 241 | 242 | func TestCommentAssociation(t *testing.T) { 243 | src := ` 244 | // foo1 245 | // foo2 246 | 247 | // bar 248 | 249 | syntax = "proto3"; 250 | 251 | // baz 252 | 253 | // bat1 254 | // bat2 255 | package bat; 256 | 257 | // Oneway is the return type to use for an rpc method if 258 | // the method should be generated as oneway. 259 | message Oneway { 260 | bool ack = 1; 261 | }` 262 | p := newParserOn(src) 263 | def, err := p.Parse() 264 | if err != nil { 265 | t.Fatal(err) 266 | } 267 | if got, want := len(def.Elements), 6; got != want { 268 | t.Fatalf("got [%v] want [%v]", got, want) 269 | } 270 | pkg := def.Elements[4].(*Package) 271 | if got, want := pkg.Comment.Message(), " bat1"; got != want { 272 | t.Fatalf("got [%v] want [%v]", got, want) 273 | } 274 | if got, want := len(pkg.Comment.Lines), 2; got != want { 275 | t.Fatalf("got [%v] want [%v]", got, want) 276 | } 277 | if got, want := pkg.Comment.Lines[1], " bat2"; got != want { 278 | t.Fatalf("got [%v] want [%v]", got, want) 279 | } 280 | if got, want := len(def.Elements[5].(*Message).Comment.Lines), 2; got != want { 281 | t.Fatalf("got [%v] want [%v]", got, want) 282 | } 283 | } 284 | 285 | func TestCommentInOptionValue(t *testing.T) { 286 | src := `syntax = "proto3"; 287 | message Foo { 288 | string bar = 1 [ 289 | // comment 290 | // me 291 | deprecated=true 292 | ]; 293 | }` 294 | p := newParserOn(src) 295 | def, err := p.Parse() 296 | if err != nil { 297 | t.Fatal(err) 298 | } 299 | o := def.Elements[1].(*Message).elements()[0].(*NormalField).Options[0] 300 | if got, want := o.Name, "deprecated"; got != want { 301 | t.Fatalf("got [%v] want [%v]", got, want) 302 | } 303 | if got, want := def.Elements[1].(*Message).elements()[0].(*NormalField).IsDeprecated(), true; got != want { 304 | t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) 305 | } 306 | if got, want := len(o.Comment.Lines), 2; got != want { 307 | t.Fatalf("got [%v] want [%v]", got, want) 308 | } 309 | if got, want := o.Comment.Lines[1], " me"; got != want { 310 | t.Fatalf("got [%v] want [%v]", got, want) 311 | } 312 | } 313 | 314 | func TestNormalFieldInlineComment(t *testing.T) { 315 | src := `message Example { 316 | /* i'm */ optional /* a comment */ bool /* too */ field /* hard */ = /* to */ 1 /* read */; 317 | }` 318 | p := newParserOn(src) 319 | def, err := p.Parse() 320 | if err != nil { 321 | t.Fatal(err) 322 | } 323 | m := def.Elements[0].(*Message) 324 | f := m.Elements[1].(*NormalField) 325 | lines := f.Field.InlineComment.Lines 326 | if got, want := len(lines), 5; got != want { 327 | t.Fatalf("got [%v:%T] want [%v:%T]", got, got, want, want) 328 | } 329 | if got, want := lines[0], " a comment "; got != want { 330 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 331 | } 332 | if got, want := lines[1], " too "; got != want { 333 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 334 | } 335 | if got, want := lines[4], " read "; got != want { 336 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /edition.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | type Edition struct { 31 | Position scanner.Position 32 | Comment *Comment 33 | Value string 34 | InlineComment *Comment 35 | Parent Visitee 36 | } 37 | 38 | func (e *Edition) parse(p *Parser) error { 39 | if _, tok, lit := p.next(); tok != tEQUALS { 40 | return p.unexpected(lit, "edition =", e) 41 | } 42 | _, _, lit := p.next() 43 | if !isString(lit) { 44 | return p.unexpected(lit, "edition string constant", e) 45 | } 46 | e.Value, _ = unQuote(lit) 47 | return nil 48 | } 49 | 50 | // Accept dispatches the call to the visitor. 51 | func (e *Edition) Accept(v Visitor) { 52 | // v.VisitEdition(e) in v2 53 | } 54 | 55 | // Doc is part of Documented 56 | func (e *Edition) Doc() *Comment { 57 | return e.Comment 58 | } 59 | 60 | // inlineComment is part of commentInliner. 61 | func (e *Edition) inlineComment(c *Comment) { 62 | e.InlineComment = c 63 | } 64 | 65 | func (e *Edition) parent(v Visitee) { e.Parent = v } 66 | -------------------------------------------------------------------------------- /edition_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestEdition(t *testing.T) { 29 | proto := `edition = "1967";` 30 | p := newParserOn(proto) 31 | pr, err := p.Parse() 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | e := pr.elements()[0].(*Edition) 36 | if got, want := e.Value, "1967"; got != want { 37 | t.Errorf("got [%v] want [%v]", got, want) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /enum.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Enum definition consists of a name and an enum body. 31 | type Enum struct { 32 | Position scanner.Position 33 | Comment *Comment 34 | Name string 35 | Elements []Visitee 36 | Parent Visitee 37 | } 38 | 39 | // Accept dispatches the call to the visitor. 40 | func (e *Enum) Accept(v Visitor) { 41 | v.VisitEnum(e) 42 | } 43 | 44 | // Doc is part of Documented 45 | func (e *Enum) Doc() *Comment { 46 | return e.Comment 47 | } 48 | 49 | // addElement is part of elementContainer 50 | func (e *Enum) addElement(v Visitee) { 51 | v.parent(e) 52 | e.Elements = append(e.Elements, v) 53 | } 54 | 55 | // elements is part of elementContainer 56 | func (e *Enum) elements() []Visitee { 57 | return e.Elements 58 | } 59 | 60 | // takeLastComment is part of elementContainer 61 | // removes and returns the last element of the list if it is a Comment. 62 | func (e *Enum) takeLastComment(expectedOnLine int) (last *Comment) { 63 | last, e.Elements = takeLastCommentIfEndsOnLine(e.Elements, expectedOnLine) 64 | return 65 | } 66 | 67 | func (e *Enum) parse(p *Parser) error { 68 | pos, tok, lit := p.next() 69 | if tok != tIDENT { 70 | if !isKeyword(tok) { 71 | return p.unexpected(lit, "enum identifier", e) 72 | } 73 | } 74 | e.Name = lit 75 | consumeCommentFor(p, e) 76 | _, tok, lit = p.next() 77 | if tok != tLEFTCURLY { 78 | return p.unexpected(lit, "enum opening {", e) 79 | } 80 | for { 81 | pos, tok, lit = p.next() 82 | switch tok { 83 | case tCOMMENT: 84 | if com := mergeOrReturnComment(e.elements(), lit, pos); com != nil { // not merged? 85 | e.addElement(com) 86 | } 87 | case tOPTION: 88 | v := new(Option) 89 | v.Position = pos 90 | v.Comment = e.takeLastComment(pos.Line) 91 | err := v.parse(p) 92 | if err != nil { 93 | return err 94 | } 95 | e.addElement(v) 96 | case tRIGHTCURLY, tEOF: 97 | goto done 98 | case tSEMICOLON: 99 | maybeScanInlineComment(p, e) 100 | case tRESERVED: 101 | r := new(Reserved) 102 | r.Position = pos 103 | r.Comment = e.takeLastComment(pos.Line - 1) 104 | if err := r.parse(p); err != nil { 105 | return err 106 | } 107 | e.addElement(r) 108 | default: 109 | p.nextPut(pos, tok, lit) 110 | f := new(EnumField) 111 | f.Position = pos 112 | f.Comment = e.takeLastComment(pos.Line - 1) 113 | err := f.parse(p) 114 | if err != nil { 115 | return err 116 | } 117 | e.addElement(f) 118 | } 119 | } 120 | done: 121 | if tok != tRIGHTCURLY { 122 | return p.unexpected(lit, "enum closing }", e) 123 | } 124 | return nil 125 | } 126 | 127 | // parent is part of elementContainer 128 | func (e *Enum) parent(p Visitee) { e.Parent = p } 129 | 130 | // EnumField is part of the body of an Enum. 131 | type EnumField struct { 132 | Position scanner.Position 133 | Comment *Comment 134 | Name string 135 | Integer int 136 | // ValueOption is deprecated, use Elements instead 137 | ValueOption *Option 138 | Elements []Visitee // such as Option and Comment 139 | InlineComment *Comment 140 | Parent Visitee 141 | } 142 | 143 | // elements is part of elementContainer 144 | func (f *EnumField) elements() []Visitee { 145 | return f.Elements 146 | } 147 | 148 | // takeLastComment is part of elementContainer 149 | // removes and returns the last element of the list if it is a Comment. 150 | func (f *EnumField) takeLastComment(expectedOnLine int) (last *Comment) { 151 | last, f.Elements = takeLastCommentIfEndsOnLine(f.Elements, expectedOnLine) 152 | return 153 | } 154 | 155 | // Accept dispatches the call to the visitor. 156 | func (f *EnumField) Accept(v Visitor) { 157 | v.VisitEnumField(f) 158 | } 159 | 160 | // inlineComment is part of commentInliner. 161 | func (f *EnumField) inlineComment(c *Comment) { 162 | f.InlineComment = c 163 | } 164 | 165 | // Doc is part of Documented 166 | func (f *EnumField) Doc() *Comment { 167 | return f.Comment 168 | } 169 | 170 | func (f *EnumField) parse(p *Parser) error { 171 | _, tok, lit := p.nextIdentifier() 172 | if tok != tIDENT { 173 | if !isKeyword(tok) { 174 | return p.unexpected(lit, "enum field identifier", f) 175 | } 176 | } 177 | f.Name = lit 178 | pos, tok, lit := p.next() 179 | if tok != tEQUALS { 180 | return p.unexpected(lit, "enum field =", f) 181 | } 182 | i, err := p.nextInteger() 183 | if err != nil { 184 | return p.unexpected(err.Error(), "enum field integer", f) 185 | } 186 | f.Integer = i 187 | pos, tok, lit = p.next() 188 | if tok == tLEFTSQUARE { 189 | for { 190 | o := new(Option) 191 | o.Position = pos 192 | o.IsEmbedded = true 193 | err := o.parse(p) 194 | if err != nil { 195 | return err 196 | } 197 | // update deprecated field with the last option found 198 | f.ValueOption = o 199 | f.addElement(o) 200 | pos, tok, lit = p.next() 201 | if tok == tCOMMA { 202 | continue 203 | } 204 | if tok == tRIGHTSQUARE { 205 | break 206 | } 207 | } 208 | } 209 | if tSEMICOLON == tok { 210 | p.nextPut(pos, tok, lit) // put back this token for scanning inline comment 211 | } 212 | return nil 213 | } 214 | 215 | // addElement is part of elementContainer 216 | func (f *EnumField) addElement(v Visitee) { 217 | v.parent(f) 218 | f.Elements = append(f.Elements, v) 219 | } 220 | 221 | func (f *EnumField) parent(v Visitee) { f.Parent = v } 222 | 223 | // IsDeprecated returns true if the option "deprecated" is set with value "true". 224 | func (f *EnumField) IsDeprecated() bool { 225 | for _, each := range f.Elements { 226 | if opt, ok := each.(*Option); ok { 227 | if opt.Name == optionNameDeprecated { 228 | return opt.Constant.Source == "true" 229 | } 230 | } 231 | } 232 | return false 233 | } 234 | -------------------------------------------------------------------------------- /enum_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "testing" 28 | ) 29 | 30 | func TestEnum(t *testing.T) { 31 | proto := ` 32 | // enum 33 | enum EnumAllowingAlias { 34 | reserved 998, 1000 to 2000; 35 | reserved "HELLO", "WORLD"; 36 | option allow_alias = true; 37 | UNKNOWN = 0; 38 | STARTED = 1; 39 | RUNNING = 2 [(custom_option) = "hello world"]; 40 | NEG = -42; 41 | SOMETHING_FOO = 0 [ 42 | (bar.enum_value_option) = true, 43 | (bar.enum_value_dep_option) = { hello: 1 } 44 | ]; 45 | }` 46 | p := newParserOn(proto) 47 | pr, err := p.Parse() 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | enums := collect(pr).Enums() 52 | if got, want := len(enums), 1; got != want { 53 | t.Errorf("got [%v] want [%v]", got, want) 54 | } 55 | if got, want := len(enums[0].Elements), 8; got != want { 56 | t.Errorf("got [%v] want [%v]", got, want) 57 | } 58 | if got, want := enums[0].Comment != nil, true; got != want { 59 | t.Fatalf("got [%v] want [%v]", got, want) 60 | } 61 | if got, want := enums[0].Comment.Message(), " enum"; got != want { 62 | t.Errorf("got [%v] want [%v]", enums[0].Comment, want) 63 | } 64 | if got, want := enums[0].Position.Line, 3; got != want { 65 | t.Errorf("got [%d] want [%d]", got, want) 66 | } 67 | // enum reserved ids 68 | e1 := enums[0].Elements[0].(*Reserved) 69 | if got, want := len(e1.Ranges), 2; got != want { 70 | t.Errorf("got [%d] want [%d]", got, want) 71 | } 72 | e1rg0 := e1.Ranges[0] 73 | if got, want := e1rg0.From, 998; got != want { 74 | t.Errorf("got [%d] want [%d]", got, want) 75 | } 76 | if got, want := e1rg0.From, e1rg0.To; got != want { 77 | t.Errorf("got [%d] want [%d]", got, want) 78 | } 79 | e1rg1 := e1.Ranges[1] 80 | if got, want := e1rg1.From, 1000; got != want { 81 | t.Errorf("got [%d] want [%d]", got, want) 82 | } 83 | if got, want := e1rg1.To, 2000; got != want { 84 | t.Errorf("got [%d] want [%d]", got, want) 85 | } 86 | // enum reserved field names 87 | e2 := enums[0].Elements[1].(*Reserved) 88 | if got, want := len(e2.FieldNames), 2; got != want { 89 | t.Errorf("got [%d] want [%d]", got, want) 90 | } 91 | e2fn0 := e2.FieldNames[0] 92 | if got, want := e2fn0, "HELLO"; got != want { 93 | t.Errorf("got [%s] want [%s]", got, want) 94 | } 95 | e2fn1 := e2.FieldNames[1] 96 | if got, want := e2fn1, "WORLD"; got != want { 97 | t.Errorf("got [%s] want [%s]", got, want) 98 | } 99 | // enum option 100 | checkParent(enums[0].Elements[2].(*Option), t) 101 | // enum values 102 | ef1 := enums[0].Elements[3].(*EnumField) 103 | if got, want := ef1.Integer, 0; got != want { 104 | t.Errorf("got [%v] want [%v]", got, want) 105 | } 106 | if got, want := ef1.Position.Line, 7; got != want { 107 | t.Errorf("got [%d] want [%d]", got, want) 108 | } 109 | ef3 := enums[0].Elements[5].(*EnumField) 110 | if got, want := ef3.Integer, 2; got != want { 111 | t.Errorf("got [%v] want [%v]", got, want) 112 | } 113 | ef3opt := ef3.Elements[0].(*Option) 114 | if got, want := ef3opt.Name, "(custom_option)"; got != want { 115 | t.Errorf("got [%v] want [%v]", got, want) 116 | } 117 | checkParent(ef3.Elements[0].(*Option), t) 118 | // test for deprecated field 119 | if got, want := ef3opt, ef3.ValueOption; got != want { 120 | t.Errorf("got [%v] want [%v]", got, want) 121 | } 122 | if got, want := ef3opt.Constant.Source, "hello world"; got != want { 123 | t.Errorf("got [%v] want [%v]", got, want) 124 | } 125 | if got, want := ef3.Position.Line, 9; got != want { 126 | t.Errorf("got [%d] want [%d]", got, want) 127 | } 128 | ef4 := enums[0].Elements[6].(*EnumField) 129 | if got, want := ef4.Integer, -42; got != want { 130 | t.Errorf("got [%v] want [%v]", got, want) 131 | } 132 | } 133 | 134 | func TestEnumWithHex(t *testing.T) { 135 | src := `enum Flags { 136 | FLAG1 = 0x11; 137 | }` 138 | p := newParserOn(src) 139 | enum := new(Enum) 140 | p.next() 141 | if err := enum.parse(p); err != nil { 142 | t.Fatal(err) 143 | } 144 | if got, want := len(enum.Elements), 1; got != want { 145 | t.Errorf("got [%v] want [%v]", got, want) 146 | } 147 | if got, want := enum.Elements[0].(*EnumField).Integer, 17; got != want { 148 | t.Errorf("got [%v] want [%v]", got, want) 149 | } 150 | } 151 | 152 | func TestEnumWithDeprecatedField(t *testing.T) { 153 | src := `enum Flags { 154 | OLD = 1 [deprecated = true]; 155 | NEW = 2; 156 | }` 157 | p := newParserOn(src) 158 | enum := new(Enum) 159 | p.next() 160 | if err := enum.parse(p); err != nil { 161 | t.Fatal(err) 162 | } 163 | if got, want := len(enum.Elements), 2; got != want { 164 | t.Errorf("got [%v] want [%v]", got, want) 165 | } 166 | if got, want := enum.Elements[0].(*EnumField).IsDeprecated(), true; got != want { 167 | t.Errorf("got [%v] want [%v]", got, want) 168 | } 169 | if got, want := enum.Elements[1].(*EnumField).IsDeprecated(), false; got != want { 170 | t.Errorf("got [%v] want [%v]", got, want) 171 | } 172 | } 173 | 174 | func TestEnumInlineCommentBeforeBody(t *testing.T) { 175 | src := `enum BarEnum // BarEnum 176 | // with another line 177 | { 178 | BAR_TYPE_INVALID= 0; 179 | BAR_TYPE_BAD = 1; 180 | } 181 | ` 182 | p := newParserOn(src) 183 | e := new(Enum) 184 | p.next() 185 | if err := e.parse(p); err != nil { 186 | t.Fatal(err) 187 | } 188 | nestedComment := e.Elements[0].(*Comment) 189 | if nestedComment == nil { 190 | t.Fatal("expected comment present") 191 | } 192 | if got, want := len(nestedComment.Lines), 2; got != want { 193 | t.Errorf("got %d want %d lines", got, want) 194 | } 195 | } 196 | 197 | func TestEnumFieldWalkWithComment(t *testing.T) { 198 | src := `enum HideIt 199 | { 200 | PRIVATE = 1 [ 201 | // hidden 202 | // field 203 | (google.api.value_visibility).restriction = "HIDDEN" 204 | ]; 205 | } 206 | ` 207 | p := newParserOn(src) 208 | e := new(Enum) 209 | p.next() 210 | if err := e.parse(p); err != nil { 211 | t.Fatal(err) 212 | } 213 | var name, msg string 214 | walk(e, WithOption(func(o *Option) { 215 | name = o.Name 216 | msg = o.Comment.Message() 217 | })) 218 | if got, want := name, "(google.api.value_visibility).restriction"; got != want { 219 | t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) 220 | } 221 | if got, want := msg, " hidden"; got != want { 222 | t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /extensions.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Extensions declare that a range of field numbers in a message are available for third-party extensions. 31 | // proto2 only 32 | type Extensions struct { 33 | Position scanner.Position 34 | Comment *Comment 35 | Ranges []Range 36 | InlineComment *Comment 37 | Parent Visitee 38 | } 39 | 40 | // inlineComment is part of commentInliner. 41 | func (e *Extensions) inlineComment(c *Comment) { 42 | e.InlineComment = c 43 | } 44 | 45 | // Accept dispatches the call to the visitor. 46 | func (e *Extensions) Accept(v Visitor) { 47 | v.VisitExtensions(e) 48 | } 49 | 50 | // parse expects ranges 51 | func (e *Extensions) parse(p *Parser) error { 52 | list, err := parseRanges(p, e) 53 | if err != nil { 54 | return err 55 | } 56 | e.Ranges = list 57 | return nil 58 | } 59 | 60 | // parent is part of elementContainer 61 | func (e *Extensions) parent(p Visitee) { e.Parent = p } 62 | -------------------------------------------------------------------------------- /extensions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestExtensions(t *testing.T) { 29 | proto := `message M { 30 | // extensions 31 | extensions 4, 20 to max; // max 32 | }` 33 | p := newParserOn(proto) 34 | p.next() // consume message 35 | m := new(Message) 36 | err := m.parse(p) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if len(m.Elements) != 1 { 41 | t.Fatal("1 elements expected, got", len(m.Elements), m.Elements) 42 | } 43 | f := m.Elements[0].(*Extensions) 44 | if got, want := len(f.Ranges), 2; got != want { 45 | t.Fatalf("got [%d] want [%d]", got, want) 46 | } 47 | if got, want := f.Position.Line, 3; got != want { 48 | t.Fatalf("got [%d] want [%d]", got, want) 49 | } 50 | if got, want := f.Ranges[1].SourceRepresentation(), "20 to max"; got != want { 51 | t.Errorf("got [%s] want [%s]", got, want) 52 | } 53 | if f.Comment == nil { 54 | t.Fatal("comment expected") 55 | } 56 | if got, want := f.InlineComment.Message(), " max"; got != want { 57 | t.Errorf("got [%s] want [%s]", got, want) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Field is an abstract message field. 31 | type Field struct { 32 | Position scanner.Position 33 | Comment *Comment 34 | Name string 35 | Type string 36 | Sequence int 37 | Options []*Option 38 | InlineComment *Comment 39 | Parent Visitee 40 | } 41 | 42 | // inlineComment is part of commentInliner. 43 | func (f *Field) inlineComment(c *Comment) { 44 | f.InlineComment = c 45 | } 46 | 47 | // NormalField represents a field in a Message. 48 | type NormalField struct { 49 | *Field 50 | Repeated bool 51 | Optional bool // proto2 52 | Required bool // proto2 53 | } 54 | 55 | func newNormalField() *NormalField { return &NormalField{Field: new(Field)} } 56 | 57 | // Accept dispatches the call to the visitor. 58 | func (f *NormalField) Accept(v Visitor) { 59 | v.VisitNormalField(f) 60 | } 61 | 62 | // Doc is part of Documented 63 | func (f *NormalField) Doc() *Comment { 64 | return f.Comment 65 | } 66 | 67 | // parse expects: 68 | // [ "repeated" | "optional" ] type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";" 69 | func (f *NormalField) parse(p *Parser) error { 70 | for { 71 | pos, tok, lit := p.nextTypeName() 72 | switch tok { 73 | case tCOMMENT: 74 | c := newComment(pos, lit) 75 | if f.InlineComment == nil { 76 | f.InlineComment = c 77 | } else { 78 | f.InlineComment.Merge(c) 79 | } 80 | case tREPEATED: 81 | f.Repeated = true 82 | return f.parse(p) 83 | case tOPTIONAL: // proto2 84 | f.Optional = true 85 | return f.parse(p) 86 | case tIDENT: 87 | f.Type = lit 88 | return parseFieldAfterType(f.Field, p, f) 89 | default: 90 | goto done 91 | } 92 | } 93 | done: 94 | return nil 95 | } 96 | 97 | // parseFieldAfterType expects: 98 | // fieldName "=" fieldNumber [ "[" fieldOptions "]" ] "; 99 | func parseFieldAfterType(f *Field, p *Parser, parent Visitee) error { 100 | expectedToken := tIDENT 101 | expected := "field identifier" 102 | 103 | for { 104 | pos, tok, lit := p.next() 105 | if tok == tCOMMENT { 106 | c := newComment(pos, lit) 107 | if f.InlineComment == nil { 108 | f.InlineComment = c 109 | } else { 110 | f.InlineComment.Merge(c) 111 | } 112 | continue 113 | } 114 | if tok != expectedToken { 115 | // allow keyword as field name 116 | if expectedToken == tIDENT && isKeyword(tok) { 117 | // continue as identifier 118 | tok = tIDENT 119 | } else { 120 | return p.unexpected(lit, expected, f) 121 | } 122 | } 123 | // found expected token 124 | if tok == tIDENT { 125 | f.Name = lit 126 | expectedToken = tEQUALS 127 | expected = "field =" 128 | continue 129 | } 130 | if tok == tEQUALS { 131 | expectedToken = tNUMBER 132 | expected = "field sequence number" 133 | continue 134 | } 135 | if tok == tNUMBER { 136 | // put it back so we can use the generic nextInteger 137 | p.nextPut(pos, tok, lit) 138 | i, err := p.nextInteger() 139 | if err != nil { 140 | return p.unexpected(lit, expected, f) 141 | } 142 | f.Sequence = i 143 | break 144 | } 145 | } 146 | consumeFieldComments(f, p) 147 | 148 | // see if there are options 149 | pos, tok, lit := p.next() 150 | if tLEFTSQUARE != tok { 151 | p.nextPut(pos, tok, lit) 152 | return nil 153 | } 154 | // consume options 155 | for { 156 | o := new(Option) 157 | o.Position = pos 158 | o.IsEmbedded = true 159 | o.parent(parent) 160 | err := o.parse(p) 161 | if err != nil { 162 | return err 163 | } 164 | f.Options = append(f.Options, o) 165 | 166 | pos, tok, lit = p.next() 167 | if tRIGHTSQUARE == tok { 168 | break 169 | } 170 | if tCOMMA != tok { 171 | return p.unexpected(lit, "option ,", o) 172 | } 173 | } 174 | return nil 175 | } 176 | 177 | func consumeFieldComments(f *Field, p *Parser) { 178 | pos, tok, lit := p.next() 179 | for tok == tCOMMENT { 180 | c := newComment(pos, lit) 181 | if f.InlineComment == nil { 182 | f.InlineComment = c 183 | } else { 184 | f.InlineComment.Merge(c) 185 | } 186 | pos, tok, lit = p.next() 187 | } 188 | // no longer a comment, put it back 189 | p.nextPut(pos, tok, lit) 190 | } 191 | 192 | // TODO copy paste 193 | func consumeOptionComments(o *Option, p *Parser) { 194 | pos, tok, lit := p.next() 195 | for tok == tCOMMENT { 196 | c := newComment(pos, lit) 197 | if o.Comment == nil { 198 | o.Comment = c 199 | } else { 200 | o.Comment.Merge(c) 201 | } 202 | pos, tok, lit = p.next() 203 | } 204 | // no longer a comment, put it back 205 | p.nextPut(pos, tok, lit) 206 | } 207 | 208 | // MapField represents a map entry in a message. 209 | type MapField struct { 210 | *Field 211 | KeyType string 212 | } 213 | 214 | func newMapField() *MapField { return &MapField{Field: new(Field)} } 215 | 216 | // Accept dispatches the call to the visitor. 217 | func (f *MapField) Accept(v Visitor) { 218 | v.VisitMapField(f) 219 | } 220 | 221 | // Doc is part of Documented 222 | func (f *MapField) Doc() *Comment { 223 | return f.Comment 224 | } 225 | 226 | // parse expects: 227 | // mapField = "map" "<" keyType "," type ">" mapName "=" fieldNumber [ "[" fieldOptions "]" ] ";" 228 | // keyType = "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" | 229 | // 230 | // "fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string" 231 | func (f *MapField) parse(p *Parser) error { 232 | _, tok, lit := p.next() 233 | if tLESS != tok { 234 | return p.unexpected(lit, "map keyType <", f) 235 | } 236 | _, tok, lit = p.nextTypeName() 237 | if tIDENT != tok { 238 | return p.unexpected(lit, "map identifier", f) 239 | } 240 | f.KeyType = lit 241 | _, tok, lit = p.next() 242 | if tCOMMA != tok { 243 | return p.unexpected(lit, "map type separator ,", f) 244 | } 245 | _, tok, lit = p.nextTypeName() 246 | if tIDENT != tok { 247 | return p.unexpected(lit, "map valueType identifier", f) 248 | } 249 | f.Type = lit 250 | _, tok, lit = p.next() 251 | if tGREATER != tok { 252 | return p.unexpected(lit, "map valueType >", f) 253 | } 254 | return parseFieldAfterType(f.Field, p, f) 255 | } 256 | 257 | func (f *Field) parent(v Visitee) { f.Parent = v } 258 | 259 | const optionNameDeprecated = "deprecated" 260 | 261 | // IsDeprecated returns true if the option "deprecated" is set with value "true". 262 | func (f *Field) IsDeprecated() bool { 263 | for _, each := range f.Options { 264 | if each.Name == optionNameDeprecated { 265 | return each.Constant.Source == "true" 266 | } 267 | } 268 | return false 269 | } 270 | -------------------------------------------------------------------------------- /field_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "testing" 28 | ) 29 | 30 | func TestField(t *testing.T) { 31 | proto := `repeated foo.bar lots =1 [option1=a, option2=b, option3="happy"];` 32 | p := newParserOn(proto) 33 | f := newNormalField() 34 | err := f.parse(p) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | if got, want := f.Repeated, true; got != want { 39 | t.Errorf("got [%v] want [%v]", got, want) 40 | } 41 | if got, want := f.Type, "foo.bar"; got != want { 42 | t.Errorf("got [%v] want [%v]", got, want) 43 | } 44 | if got, want := f.Name, "lots"; got != want { 45 | t.Errorf("got [%v] want [%v]", got, want) 46 | } 47 | if got, want := len(f.Options), 3; got != want { 48 | t.Fatalf("got [%v] want [%v]", got, want) 49 | } 50 | if got, want := f.Options[0].Name, "option1"; got != want { 51 | t.Errorf("got [%v] want [%v]", got, want) 52 | } 53 | if got, want := f.Options[0].Constant.Source, "a"; got != want { 54 | t.Errorf("got [%v] want [%v]", got, want) 55 | } 56 | if got, want := f.Options[1].Name, "option2"; got != want { 57 | t.Errorf("got [%v] want [%v]", got, want) 58 | } 59 | if got, want := f.Options[1].Constant.Source, "b"; got != want { 60 | t.Errorf("got [%v] want [%v]", got, want) 61 | } 62 | if got, want := f.Options[2].Constant.Source, "happy"; got != want { 63 | t.Errorf("got [%v] want [%v]", got, want) 64 | } 65 | checkParent(f.Options[0], t) 66 | } 67 | 68 | func TestFieldNoWhitespace(t *testing.T) { 69 | proto := `string s=1[a=b];` 70 | p := newParserOn(proto) 71 | f := newNormalField() 72 | err := f.parse(p) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | if got, want := f.Type, "string"; got != want { 77 | t.Errorf("got [%v] want [%v]", got, want) 78 | } 79 | if got, want := f.Sequence, 1; got != want { 80 | t.Errorf("got [%v] want [%v]", got, want) 81 | } 82 | if got, want := f.Options[0].Name, "a"; got != want { 83 | t.Errorf("got [%v] want [%v]", got, want) 84 | } 85 | if got, want := f.Options[0].Constant.Source, "b"; got != want { 86 | t.Errorf("got [%v] want [%v]", got, want) 87 | } 88 | } 89 | 90 | func TestFieldSimple(t *testing.T) { 91 | proto := `string optional_string_piece = 24 [ctype=STRING_PIECE];` 92 | p := newParserOn(proto) 93 | f := newNormalField() 94 | err := f.parse(p) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | if got, want := f.Type, "string"; got != want { 99 | t.Errorf("got [%v] want [%v]", got, want) 100 | } 101 | if got, want := f.Name, "optional_string_piece"; got != want { 102 | t.Errorf("got [%v] want [%v]", got, want) 103 | } 104 | if got, want := f.Sequence, 24; got != want { 105 | t.Errorf("got [%v] want [%v]", got, want) 106 | } 107 | if got, want := len(f.Options), 1; got != want { 108 | t.Fatalf("got [%v] want [%v]", got, want) 109 | } 110 | if got, want := f.Options[0].Name, "ctype"; got != want { 111 | t.Errorf("got [%v] want [%v]", got, want) 112 | } 113 | if got, want := f.Options[0].Constant.Source, "STRING_PIECE"; got != want { 114 | t.Errorf("got [%v] want [%v]", got, want) 115 | } 116 | } 117 | 118 | func TestFieldSyntaxErrors(t *testing.T) { 119 | for i, each := range []string{ 120 | `repeatet foo.bar lots = 1;`, 121 | `string lots === 1;`, 122 | } { 123 | f := newNormalField() 124 | if f.parse(newParserOn(each)) == nil { 125 | t.Errorf("uncaught syntax error in test case %d, %#v", i, f) 126 | } 127 | } 128 | } 129 | 130 | func TestMapField(t *testing.T) { 131 | proto := ` projects = 3 [foo=bar];` 132 | p := newParserOn(proto) 133 | f := newMapField() 134 | err := f.parse(p) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | if got, want := f.KeyType, "string"; got != want { 139 | t.Errorf("got [%v] want [%v]", got, want) 140 | } 141 | if got, want := f.Type, "Project"; got != want { 142 | t.Errorf("got [%v] want [%v]", got, want) 143 | } 144 | if got, want := f.Name, "projects"; got != want { 145 | t.Errorf("got [%v] want [%v]", got, want) 146 | } 147 | if got, want := f.Sequence, 3; got != want { 148 | t.Errorf("got [%v] want [%v]", got, want) 149 | } 150 | checkParent(f.Options[0], t) 151 | } 152 | 153 | func TestMapFieldWithDotTypes(t *testing.T) { 154 | proto := ` <.Some.How, .Such.Project> projects = 3;` 155 | p := newParserOn(proto) 156 | f := newMapField() 157 | err := f.parse(p) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | if got, want := f.KeyType, ".Some.How"; got != want { 162 | t.Errorf("got [%v] want [%v]", got, want) 163 | } 164 | if got, want := f.Type, ".Such.Project"; got != want { 165 | t.Errorf("got [%v] want [%v]", got, want) 166 | } 167 | } 168 | 169 | func TestOptionalWithOption(t *testing.T) { 170 | proto := `optional int32 default_int32 = 61 [default = 41 ];` 171 | p := newParserOn(proto) 172 | f := newNormalField() 173 | err := f.parse(p) 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | if got, want := f.Sequence, 61; got != want { 178 | t.Errorf("got [%v] want [%v]", got, want) 179 | } 180 | o := f.Options[0] 181 | if got, want := o.Name, "default"; got != want { 182 | t.Errorf("got [%v] want [%v]", got, want) 183 | } 184 | if got, want := o.Constant.Source, "41"; got != want { 185 | t.Errorf("got [%v] want [%v]", got, want) 186 | } 187 | } 188 | 189 | func TestFieldInlineComment(t *testing.T) { 190 | proto := `message Hello { 191 | // comment 192 | bool foo = 1; // inline comment 193 | }` 194 | p := newParserOn(proto) 195 | def := new(Proto) 196 | err := def.parse(p) 197 | if err != nil { 198 | t.Fatal(err) 199 | } 200 | m := def.Elements[0].(*Message) 201 | if len(m.Elements) != 1 { 202 | t.Error("expected one element", m.Elements) 203 | } 204 | f := m.Elements[0].(*NormalField) 205 | if f.InlineComment == nil { 206 | t.Error("expected inline comment") 207 | } 208 | } 209 | 210 | func TestFieldTypeStartsWithDot(t *testing.T) { 211 | proto := `.game.Resource foo = 1;` 212 | p := newParserOn(proto) 213 | f := newNormalField() 214 | err := f.parse(p) 215 | if err != nil { 216 | t.Fatal(err) 217 | } 218 | foot := f.Field.Type 219 | if got, want := foot, ".game.Resource"; got != want { 220 | t.Errorf("got [%v] want [%v]", got, want) 221 | } 222 | } 223 | 224 | func TestMultiLineFieldType(t *testing.T) { 225 | src := `google.ads.googleads.v1.enums.ConversionAdjustmentTypeEnum 226 | .ConversionAdjustmentType adjustment_type = 5;` 227 | p := newParserOn(src) 228 | f := newNormalField() 229 | err := f.parse(p) 230 | if err != nil { 231 | t.Fatal(err) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emicklei/proto 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Group represents a (proto2 only) group. 31 | // https://developers.google.com/protocol-buffers/docs/reference/proto2-spec#group_field 32 | type Group struct { 33 | Position scanner.Position 34 | Comment *Comment 35 | Name string 36 | Optional bool 37 | Repeated bool 38 | Required bool 39 | Sequence int 40 | Elements []Visitee 41 | Parent Visitee 42 | } 43 | 44 | // Accept dispatches the call to the visitor. 45 | func (g *Group) Accept(v Visitor) { 46 | v.VisitGroup(g) 47 | } 48 | 49 | // addElement is part of elementContainer 50 | func (g *Group) addElement(v Visitee) { 51 | v.parent(g) 52 | g.Elements = append(g.Elements, v) 53 | } 54 | 55 | // elements is part of elementContainer 56 | func (g *Group) elements() []Visitee { 57 | return g.Elements 58 | } 59 | 60 | // Doc is part of Documented 61 | func (g *Group) Doc() *Comment { 62 | return g.Comment 63 | } 64 | 65 | // takeLastComment is part of elementContainer 66 | // removes and returns the last element of the list if it is a Comment. 67 | func (g *Group) takeLastComment(expectedOnLine int) (last *Comment) { 68 | last, g.Elements = takeLastCommentIfEndsOnLine(g.Elements, expectedOnLine) 69 | return 70 | } 71 | 72 | // parse expects: 73 | // groupName "=" fieldNumber { messageBody } 74 | func (g *Group) parse(p *Parser) error { 75 | _, tok, lit := p.next() 76 | if tok != tIDENT { 77 | if !isKeyword(tok) { 78 | return p.unexpected(lit, "group name", g) 79 | } 80 | } 81 | g.Name = lit 82 | _, tok, lit = p.next() 83 | if tok != tEQUALS { 84 | return p.unexpected(lit, "group =", g) 85 | } 86 | i, err := p.nextInteger() 87 | if err != nil { 88 | return p.unexpected(lit, "group sequence number", g) 89 | } 90 | g.Sequence = i 91 | consumeCommentFor(p, g) 92 | _, tok, lit = p.next() 93 | if tok != tLEFTCURLY { 94 | return p.unexpected(lit, "group opening {", g) 95 | } 96 | return parseMessageBody(p, g) 97 | } 98 | 99 | func (g *Group) parent(v Visitee) { g.Parent = v } 100 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestGroup(t *testing.T) { 29 | oto := `message M { 30 | // group 31 | optional group OptionalGroup = 16 // group comment 1 32 | // group comment 2 33 | { 34 | // field 35 | optional int32 a = 17; 36 | } 37 | }` 38 | p := newParserOn(oto) 39 | p.next() // consume first token 40 | m := new(Message) 41 | err := m.parse(p) 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | if got, want := len(m.Elements), 1; got != want { 46 | t.Logf("%#v", m.Elements) 47 | t.Fatalf("got [%v] want [%v]", got, want) 48 | } 49 | g := m.Elements[0].(*Group) 50 | if got, want := len(g.Elements), 2; got != want { 51 | t.Fatalf("got [%v] want [%v]", got, want) 52 | } 53 | if got, want := g.Position.Line, 3; got != want { 54 | t.Fatalf("got [%v] want [%v]", got, want) 55 | } 56 | if got, want := g.Comment != nil, true; got != want { 57 | t.Errorf("got [%v] want [%v]", got, want) 58 | } 59 | f := g.Elements[1].(*NormalField) 60 | if got, want := f.Name, "a"; got != want { 61 | t.Errorf("got [%v] want [%v]", got, want) 62 | } 63 | if got, want := f.Optional, true; got != want { 64 | t.Errorf("got [%v] want [%v]", got, want) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /import.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Import holds a filename to another .proto definition. 31 | type Import struct { 32 | Position scanner.Position 33 | Comment *Comment 34 | Filename string 35 | Kind string // weak, public, 36 | InlineComment *Comment 37 | Parent Visitee 38 | } 39 | 40 | func (i *Import) parse(p *Parser) error { 41 | _, tok, lit := p.next() 42 | switch tok { 43 | case tWEAK: 44 | i.Kind = lit 45 | return i.parse(p) 46 | case tPUBLIC: 47 | i.Kind = lit 48 | return i.parse(p) 49 | case tIDENT: 50 | i.Filename, _ = unQuote(lit) 51 | default: 52 | return p.unexpected(lit, "import classifier weak|public|quoted", i) 53 | } 54 | return nil 55 | } 56 | 57 | // Accept dispatches the call to the visitor. 58 | func (i *Import) Accept(v Visitor) { 59 | v.VisitImport(i) 60 | } 61 | 62 | // inlineComment is part of commentInliner. 63 | func (i *Import) inlineComment(c *Comment) { 64 | i.InlineComment = c 65 | } 66 | 67 | // Doc is part of Documented 68 | func (i *Import) Doc() *Comment { 69 | return i.Comment 70 | } 71 | 72 | func (i *Import) parent(v Visitee) { i.Parent = v } 73 | -------------------------------------------------------------------------------- /import_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestParseImport(t *testing.T) { 29 | proto := `import public "other.proto";` 30 | p := newParserOn(proto) 31 | p.next() // consume first token 32 | i := new(Import) 33 | err := i.parse(p) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if got, want := i.Filename, "other.proto"; got != want { 38 | t.Errorf("got [%v] want [%v]", got, want) 39 | } 40 | if got, want := i.Kind, "public"; got != want { 41 | t.Errorf("got [%v] want [%v]", got, want) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /literals.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "bytes" 28 | "sort" 29 | "text/scanner" 30 | ) 31 | 32 | // Literal represents intLit,floatLit,strLit or boolLit or a nested structure thereof. 33 | type Literal struct { 34 | Position scanner.Position 35 | Source string 36 | IsString bool 37 | 38 | // It not nil then the entry is actually a comment with line(s) 39 | // modelled this way because Literal is not an elementContainer 40 | Comment *Comment 41 | 42 | // The rune use to delimit the string value (only valid iff IsString) 43 | QuoteRune rune 44 | 45 | // literal value can be an array literal value (even nested) 46 | Array []*Literal 47 | 48 | // literal value can be a map of literals (even nested) 49 | // DEPRECATED: use OrderedMap instead 50 | Map map[string]*Literal 51 | 52 | // literal value can be a map of literals (even nested) 53 | // this is done as pairs of name keys and literal values so the original ordering is preserved 54 | OrderedMap LiteralMap 55 | } 56 | 57 | var emptyRune rune 58 | 59 | // LiteralMap is like a map of *Literal but preserved the ordering. 60 | // Can be iterated yielding *NamedLiteral values. 61 | type LiteralMap []*NamedLiteral 62 | 63 | // Get returns a Literal from the map. 64 | func (m LiteralMap) Get(key string) (*Literal, bool) { 65 | for _, each := range m { 66 | if each.Name == key { 67 | // exit on the first match 68 | return each.Literal, true 69 | } 70 | } 71 | return new(Literal), false 72 | } 73 | 74 | // SourceRepresentation returns the source (use the same rune that was used to delimit the string). 75 | func (l Literal) SourceRepresentation() string { 76 | var buf bytes.Buffer 77 | if l.IsString { 78 | if l.QuoteRune == emptyRune { 79 | buf.WriteRune('"') 80 | } else { 81 | buf.WriteRune(l.QuoteRune) 82 | } 83 | } 84 | buf.WriteString(l.Source) 85 | if l.IsString { 86 | if l.QuoteRune == emptyRune { 87 | buf.WriteRune('"') 88 | } else { 89 | buf.WriteRune(l.QuoteRune) 90 | } 91 | } 92 | return buf.String() 93 | } 94 | 95 | // parse expects to read a literal constant after =. 96 | func (l *Literal) parse(p *Parser) error { 97 | pos, tok, lit := p.next() 98 | // handle special element inside literal, a comment line 99 | if isComment(lit) { 100 | nc := newComment(pos, lit) 101 | if l.Comment == nil { 102 | l.Comment = nc 103 | } else { 104 | l.Comment.Merge(nc) 105 | } 106 | // continue with remaining entries 107 | return l.parse(p) 108 | } 109 | if tok == tLEFTSQUARE { 110 | // collect array elements 111 | array := []*Literal{} 112 | 113 | // if it's an empty array, consume the close bracket, set the Array to 114 | // an empty array, and return 115 | r := p.peekNonWhitespace() 116 | if r == ']' { 117 | pos, _, _ := p.next() 118 | l.Array = array 119 | l.IsString = false 120 | l.Position = pos 121 | return nil 122 | } 123 | for { 124 | e := new(Literal) 125 | if err := e.parse(p); err != nil { 126 | return err 127 | } 128 | array = append(array, e) 129 | _, tok, lit := p.next() 130 | if tok == tCOMMA { 131 | continue 132 | } 133 | if tok == tRIGHTSQUARE { 134 | break 135 | } 136 | return p.unexpected(lit, ", or ]", l) 137 | } 138 | l.Array = array 139 | l.IsString = false 140 | l.Position = pos 141 | return nil 142 | } 143 | if tLEFTCURLY == tok { 144 | l.Position, l.Source, l.IsString = pos, "", false 145 | constants, err := parseAggregateConstants(p, l) 146 | if err != nil { 147 | return nil 148 | } 149 | l.OrderedMap = LiteralMap(constants) 150 | return nil 151 | } 152 | if "-" == lit { 153 | // negative number 154 | if err := l.parse(p); err != nil { 155 | return err 156 | } 157 | // modify source and position 158 | l.Position, l.Source = pos, "-"+l.Source 159 | return nil 160 | } 161 | source := lit 162 | iss := isString(lit) 163 | if iss { 164 | source, l.QuoteRune = unQuote(source) 165 | } 166 | l.Position, l.Source, l.IsString = pos, source, iss 167 | 168 | // peek for multiline strings 169 | for { 170 | pos, tok, lit := p.next() 171 | if isString(lit) { 172 | line, _ := unQuote(lit) 173 | l.Source += line 174 | } else { 175 | p.nextPut(pos, tok, lit) 176 | break 177 | } 178 | } 179 | return nil 180 | } 181 | 182 | // NamedLiteral associates a name with a Literal 183 | type NamedLiteral struct { 184 | *Literal 185 | Name string 186 | // PrintsColon is true when the Name must be printed with a colon suffix 187 | PrintsColon bool 188 | } 189 | 190 | // flatten the maps of each literal, recursively 191 | // this func exists for deprecated Option.AggregatedConstants. 192 | func collectAggregatedConstants(m map[string]*Literal) (list []*NamedLiteral) { 193 | for k, v := range m { 194 | if v.Map != nil { 195 | sublist := collectAggregatedConstants(v.Map) 196 | for _, each := range sublist { 197 | list = append(list, &NamedLiteral{ 198 | Name: k + "." + each.Name, 199 | PrintsColon: true, 200 | Literal: each.Literal, 201 | }) 202 | } 203 | } else { 204 | list = append(list, &NamedLiteral{ 205 | Name: k, 206 | PrintsColon: true, 207 | Literal: v, 208 | }) 209 | } 210 | } 211 | // sort list by position of literal 212 | sort.Sort(byPosition(list)) 213 | return 214 | } 215 | 216 | type byPosition []*NamedLiteral 217 | 218 | func (b byPosition) Less(i, j int) bool { 219 | return b[i].Literal.Position.Line < b[j].Literal.Position.Line 220 | } 221 | func (b byPosition) Len() int { return len(b) } 222 | func (b byPosition) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 223 | 224 | func parseAggregateConstants(p *Parser, container interface{}) (list []*NamedLiteral, err error) { 225 | for { 226 | _, tok, lit := p.nextMessageLiteralFieldName() 227 | // if tRIGHTSQUARE == tok { 228 | // p.nextPut(pos, tok, lit) 229 | // // caller has checked for open square ; will consume rightsquare, rightcurly and semicolon 230 | // return 231 | // } 232 | if tRIGHTCURLY == tok { 233 | return 234 | } 235 | if tSEMICOLON == tok { 236 | // just consume it 237 | continue 238 | //return 239 | } 240 | if tCOMMENT == tok { 241 | // assign to last parsed literal 242 | // TODO: see TestUseOfSemicolonsInAggregatedConstants 243 | continue 244 | } 245 | if tCOMMA == tok { 246 | if len(list) == 0 { 247 | err = p.unexpected(lit, "non-empty option aggregate key", container) 248 | return 249 | } 250 | continue 251 | } 252 | if tIDENT != tok && !isKeyword(tok) { 253 | err = p.unexpected(lit, "option aggregate key", container) 254 | return 255 | } 256 | // workaround issue #59 TODO 257 | if isString(lit) && len(list) > 0 { 258 | // concatenate with previous constant 259 | s, _ := unQuote(lit) 260 | list[len(list)-1].Source += s 261 | continue 262 | } 263 | key := lit 264 | printsColon := false 265 | // expect colon, aggregate or plain literal 266 | pos, tok, lit := p.next() 267 | if tCOLON == tok { 268 | // consume it 269 | printsColon = true 270 | pos, tok, lit = p.next() 271 | } 272 | // see if nested aggregate is started 273 | if tLEFTCURLY == tok { 274 | nested, fault := parseAggregateConstants(p, container) 275 | if fault != nil { 276 | err = fault 277 | return 278 | } 279 | 280 | // create the map 281 | m := map[string]*Literal{} 282 | for _, each := range nested { 283 | m[each.Name] = each.Literal 284 | } 285 | list = append(list, &NamedLiteral{ 286 | Name: key, 287 | PrintsColon: printsColon, 288 | Literal: &Literal{Map: m, OrderedMap: LiteralMap(nested)}}) 289 | continue 290 | } 291 | // no aggregate, put back token 292 | p.nextPut(pos, tok, lit) 293 | // now we see plain literal 294 | l := new(Literal) 295 | l.Position = pos 296 | if err = l.parse(p); err != nil { 297 | return 298 | } 299 | list = append(list, &NamedLiteral{Name: key, Literal: l, PrintsColon: printsColon}) 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /literals_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestLiteralString(t *testing.T) { 29 | proto := `"string"` 30 | p := newParserOn(proto) 31 | l := new(Literal) 32 | if err := l.parse(p); err != nil { 33 | t.Fatal(err) 34 | } 35 | if got, want := l.IsString, true; got != want { 36 | t.Errorf("got [%v] want [%v]", got, want) 37 | } 38 | if got, want := l.Source, "string"; got != want { 39 | t.Errorf("got [%v] want [%v]", got, want) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Message consists of a message name and a message body. 31 | type Message struct { 32 | Position scanner.Position 33 | Comment *Comment 34 | Name string 35 | IsExtend bool 36 | Elements []Visitee 37 | Parent Visitee 38 | } 39 | 40 | func (m *Message) groupName() string { 41 | if m.IsExtend { 42 | return "extend" 43 | } 44 | return "message" 45 | } 46 | 47 | // parse expects ident { messageBody 48 | func (m *Message) parse(p *Parser) error { 49 | _, tok, lit := p.nextIdentifier() 50 | if tok != tIDENT { 51 | if !isKeyword(tok) { 52 | return p.unexpected(lit, m.groupName()+" identifier", m) 53 | } 54 | } 55 | m.Name = lit 56 | consumeCommentFor(p, m) 57 | _, tok, lit = p.next() 58 | if tok != tLEFTCURLY { 59 | return p.unexpected(lit, m.groupName()+" opening {", m) 60 | } 61 | return parseMessageBody(p, m) 62 | } 63 | 64 | // parseMessageBody parses elements after {. It consumes the closing } 65 | func parseMessageBody(p *Parser, c elementContainer) error { 66 | var ( 67 | pos scanner.Position 68 | tok token 69 | lit string 70 | ) 71 | for { 72 | pos, tok, lit = p.next() 73 | switch { 74 | case isComment(lit): 75 | if com := mergeOrReturnComment(c.elements(), lit, pos); com != nil { // not merged? 76 | c.addElement(com) 77 | } 78 | case tENUM == tok: 79 | e := new(Enum) 80 | e.Position = pos 81 | e.Comment = c.takeLastComment(pos.Line - 1) 82 | if err := e.parse(p); err != nil { 83 | return err 84 | } 85 | c.addElement(e) 86 | case tMESSAGE == tok: 87 | msg := new(Message) 88 | msg.Position = pos 89 | msg.Comment = c.takeLastComment(pos.Line - 1) 90 | if err := msg.parse(p); err != nil { 91 | return err 92 | } 93 | c.addElement(msg) 94 | case tOPTION == tok: 95 | o := new(Option) 96 | o.Position = pos 97 | o.Comment = c.takeLastComment(pos.Line - 1) 98 | if err := o.parse(p); err != nil { 99 | return err 100 | } 101 | c.addElement(o) 102 | case tONEOF == tok: 103 | o := new(Oneof) 104 | o.Position = pos 105 | o.Comment = c.takeLastComment(pos.Line - 1) 106 | if err := o.parse(p); err != nil { 107 | return err 108 | } 109 | c.addElement(o) 110 | case tMAP == tok: 111 | f := newMapField() 112 | f.Position = pos 113 | f.Comment = c.takeLastComment(pos.Line - 1) 114 | if err := f.parse(p); err != nil { 115 | return err 116 | } 117 | c.addElement(f) 118 | case tRESERVED == tok: 119 | r := new(Reserved) 120 | r.Position = pos 121 | r.Comment = c.takeLastComment(pos.Line - 1) 122 | if err := r.parse(p); err != nil { 123 | return err 124 | } 125 | c.addElement(r) 126 | // BEGIN proto2 127 | case tOPTIONAL == tok || tREPEATED == tok || tREQUIRED == tok: 128 | // look ahead 129 | prevTok := tok 130 | pos, tok, lit = p.next() 131 | if tGROUP == tok { 132 | g := new(Group) 133 | g.Position = pos 134 | g.Comment = c.takeLastComment(pos.Line - 1) 135 | g.Optional = prevTok == tOPTIONAL 136 | g.Repeated = prevTok == tREPEATED 137 | g.Required = prevTok == tREQUIRED 138 | if err := g.parse(p); err != nil { 139 | return err 140 | } 141 | c.addElement(g) 142 | } else { 143 | // not a group, will be tFIELD 144 | p.nextPut(pos, tok, lit) 145 | f := newNormalField() 146 | f.Type = lit 147 | f.Position = pos 148 | f.Comment = c.takeLastComment(pos.Line - 1) 149 | f.Optional = prevTok == tOPTIONAL 150 | f.Repeated = prevTok == tREPEATED 151 | f.Required = prevTok == tREQUIRED 152 | if err := f.parse(p); err != nil { 153 | return err 154 | } 155 | c.addElement(f) 156 | } 157 | case tGROUP == tok: 158 | g := new(Group) 159 | g.Position = pos 160 | g.Comment = c.takeLastComment(pos.Line - 1) 161 | if err := g.parse(p); err != nil { 162 | return err 163 | } 164 | c.addElement(g) 165 | case tEXTENSIONS == tok: 166 | e := new(Extensions) 167 | e.Position = pos 168 | e.Comment = c.takeLastComment(pos.Line - 1) 169 | if err := e.parse(p); err != nil { 170 | return err 171 | } 172 | c.addElement(e) 173 | case tEXTEND == tok: 174 | e := new(Message) 175 | e.Position = pos 176 | e.Comment = c.takeLastComment(pos.Line - 1) 177 | e.IsExtend = true 178 | if err := e.parse(p); err != nil { 179 | return err 180 | } 181 | c.addElement(e) 182 | // END proto2 only 183 | case tRIGHTCURLY == tok || tEOF == tok: 184 | goto done 185 | case tSEMICOLON == tok: 186 | maybeScanInlineComment(p, c) 187 | // continue 188 | default: 189 | // tFIELD 190 | p.nextPut(pos, tok, lit) 191 | f := newNormalField() 192 | f.Position = pos 193 | f.Comment = c.takeLastComment(pos.Line - 1) 194 | if err := f.parse(p); err != nil { 195 | return err 196 | } 197 | c.addElement(f) 198 | } 199 | } 200 | done: 201 | if tok != tRIGHTCURLY { 202 | return p.unexpected(lit, "extend|message|group closing }", c) 203 | } 204 | return nil 205 | } 206 | 207 | // Accept dispatches the call to the visitor. 208 | func (m *Message) Accept(v Visitor) { 209 | v.VisitMessage(m) 210 | } 211 | 212 | // addElement is part of elementContainer 213 | func (m *Message) addElement(v Visitee) { 214 | v.parent(m) 215 | m.Elements = append(m.Elements, v) 216 | } 217 | 218 | // elements is part of elementContainer 219 | func (m *Message) elements() []Visitee { 220 | return m.Elements 221 | } 222 | 223 | func (m *Message) takeLastComment(expectedOnLine int) (last *Comment) { 224 | last, m.Elements = takeLastCommentIfEndsOnLine(m.Elements, expectedOnLine) 225 | return 226 | } 227 | 228 | // Doc is part of Documented 229 | func (m *Message) Doc() *Comment { 230 | return m.Comment 231 | } 232 | 233 | func (m *Message) parent(v Visitee) { m.Parent = v } 234 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "testing" 28 | ) 29 | 30 | func TestMessage(t *testing.T) { 31 | proto := ` 32 | message Out { 33 | // identifier 34 | string id = 1; 35 | // size 36 | int64 size = 2; 37 | 38 | oneof foo { 39 | string name = 4; 40 | SubMessage sub_message = 9; 41 | } 42 | message Inner { // Level 2 43 | int64 ival = 1; 44 | } 45 | map proto2_value = 13; 46 | option (my_option).a = true; 47 | }` 48 | p := newParserOn(proto) 49 | p.next() // consume first token 50 | m := new(Message) 51 | err := m.parse(p) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | if got, want := m.Name, "Out"; got != want { 56 | t.Errorf("got [%v] want [%v]", got, want) 57 | } 58 | if got, want := len(m.Elements), 6; got != want { 59 | t.Errorf("got [%v] want [%v]", got, want) 60 | } 61 | if got, want := m.Elements[0].(*NormalField).Position.String(), ":4:3"; got != want { 62 | t.Errorf("got [%v] want [%v]", got, want) 63 | } 64 | if got, want := m.Elements[0].(*NormalField).Comment.Position.String(), ":3:3"; got != want { 65 | t.Errorf("got [%v] want [%v]", got, want) 66 | } 67 | if got, want := m.Elements[3].(*Message).Position.String(), ":12:3"; got != want { 68 | t.Errorf("got [%v] want [%v]", got, want) 69 | } 70 | if got, want := m.Elements[3].(*Message).Elements[0].(*NormalField).Position.Line, 13; got != want { 71 | t.Errorf("got [%v] want [%v]", got, want) 72 | } 73 | checkParent(m.Elements[5].(*Option), t) 74 | } 75 | 76 | func TestRepeatedGroupInMessage(t *testing.T) { 77 | src := `message SearchResponse { 78 | repeated group Result = 1 { 79 | required string url = 2; 80 | optional string title = 3; 81 | repeated string snippets = 4; 82 | } 83 | }` 84 | p := newParserOn(src) 85 | p.next() // consume first token 86 | m := new(Message) 87 | err := m.parse(p) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | if got, want := len(m.Elements), 1; got != want { 92 | t.Logf("%#v", m.Elements) 93 | t.Fatalf("got [%v] want [%v]", got, want) 94 | } 95 | g := m.Elements[0].(*Group) 96 | if got, want := len(g.Elements), 3; got != want { 97 | t.Fatalf("got [%v] want [%v]", got, want) 98 | } 99 | if got, want := g.Repeated, true; got != want { 100 | t.Fatalf("got Repeated [%v] want [%v]", got, want) 101 | } 102 | 103 | } 104 | 105 | func TestRequiredGroupInMessage(t *testing.T) { 106 | src := `message SearchResponse { 107 | required group Result = 1 { 108 | required string url = 2; 109 | optional string title = 3; 110 | repeated string snippets = 4; 111 | } 112 | }` 113 | p := newParserOn(src) 114 | p.next() // consume first token 115 | m := new(Message) 116 | err := m.parse(p) 117 | if err != nil { 118 | t.Error(err) 119 | } 120 | if got, want := len(m.Elements), 1; got != want { 121 | t.Logf("%#v", m.Elements) 122 | t.Fatalf("got [%v] want [%v]", got, want) 123 | } 124 | g := m.Elements[0].(*Group) 125 | if got, want := len(g.Elements), 3; got != want { 126 | t.Fatalf("got [%v] want [%v]", got, want) 127 | } 128 | if got, want := g.Required, true; got != want { 129 | t.Fatalf("got Required [%v] want [%v]", got, want) 130 | } 131 | 132 | } 133 | 134 | func TestSingleQuotedReservedNames(t *testing.T) { 135 | src := `message Channel { 136 | reserved '', 'things', ""; 137 | }` 138 | p := newParserOn(src) 139 | p.next() // consume first token 140 | m := new(Message) 141 | err := m.parse(p) 142 | if err != nil { 143 | t.Error(err) 144 | } 145 | r := m.Elements[0].(*Reserved) 146 | if got, want := r.FieldNames[0], ""; got != want { 147 | t.Fatalf("got [%v] want [%v]", got, want) 148 | } 149 | if got, want := r.FieldNames[1], "things"; got != want { 150 | t.Fatalf("got [%v] want [%v]", got, want) 151 | } 152 | if got, want := r.FieldNames[2], ""; got != want { 153 | t.Fatalf("got [%v] want [%v]", got, want) 154 | } 155 | } 156 | 157 | func TestMessageInlineCommentBeforeBody(t *testing.T) { 158 | src := `message BarMessage // BarMessage 159 | // with another line 160 | { 161 | name string = 1; 162 | } 163 | ` 164 | p := newParserOn(src) 165 | msg := new(Message) 166 | p.next() 167 | if err := msg.parse(p); err != nil { 168 | t.Fatal(err) 169 | } 170 | nestedComment := msg.Elements[0].(*Comment) 171 | if nestedComment == nil { 172 | t.Fatal("expected comment present") 173 | } 174 | if got, want := len(nestedComment.Lines), 2; got != want { 175 | t.Errorf("got %d want %d lines", got, want) 176 | } 177 | } 178 | 179 | func TestMessageWithMessage(t *testing.T) { 180 | src := `message message { 181 | string message = 1; 182 | } 183 | ` 184 | p := newParserOn(src) 185 | msg := new(Message) 186 | p.next() 187 | if err := msg.parse(p); err != nil { 188 | t.Fatal(err) 189 | } 190 | if got, want := msg.Name, "message"; got != want { 191 | t.Errorf("got %s want %s", got, want) 192 | } 193 | if got, want := len(msg.Elements), 1; got != want { 194 | t.Errorf("got %d want %d elements", got, want) 195 | } 196 | f := msg.Elements[0].(*NormalField) 197 | if got, want := f.Name, "message"; got != want { 198 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 199 | } 200 | } 201 | 202 | func TestIssue143_Key(t *testing.T) { 203 | src := `message Msg { 204 | option (option_name) = { [key]: value_name }; 205 | }` 206 | p := newParserOn(src) 207 | msg := new(Message) 208 | p.next() 209 | if err := msg.parse(p); err != nil { 210 | t.Fatal(err) 211 | } 212 | name := msg.Elements[0].(*Option).AggregatedConstants[0].Name 213 | value := msg.Elements[0].(*Option).AggregatedConstants[0].Literal.Source 214 | if got, want := name, "key"; got != want { 215 | t.Errorf("got %s want %s", got, want) 216 | } 217 | if got, want := value, "value_name"; got != want { 218 | t.Errorf("got %s want %s", got, want) 219 | } 220 | } 221 | func TestIssue143_KeyDot(t *testing.T) { 222 | src := `message Msg { 223 | option (option_name) = { [key.dot]: value_name }; 224 | }` 225 | p := newParserOn(src) 226 | msg := new(Message) 227 | p.next() 228 | if err := msg.parse(p); err != nil { 229 | t.Fatal(err) 230 | } 231 | name := msg.Elements[0].(*Option).AggregatedConstants[0].Name 232 | value := msg.Elements[0].(*Option).AggregatedConstants[0].Literal.Source 233 | if got, want := name, "key.dot"; got != want { 234 | t.Errorf("got %s want %s", got, want) 235 | } 236 | if got, want := value, "value_name"; got != want { 237 | t.Errorf("got %s want %s", got, want) 238 | } 239 | } 240 | func TestIssue143_Keyword(t *testing.T) { 241 | src := `message Msg { 242 | option (option_name) = { [option.message]: repeated }; 243 | }` 244 | p := newParserOn(src) 245 | msg := new(Message) 246 | p.next() 247 | if err := msg.parse(p); err != nil { 248 | t.Fatal(err) 249 | } 250 | name := msg.Elements[0].(*Option).AggregatedConstants[0].Name 251 | value := msg.Elements[0].(*Option).AggregatedConstants[0].Literal.Source 252 | if got, want := name, "option.message"; got != want { 253 | t.Errorf("got %s want %s", got, want) 254 | } 255 | if got, want := value, "repeated"; got != want { 256 | t.Errorf("got %s want %s", got, want) 257 | } 258 | } 259 | func TestCommentsInFieldOptionsArray(t *testing.T) { 260 | src := `message Msg { 261 | repeated string strings_list = 5 [ 262 | // before 263 | (validate.rules).repeated.max_items = 20 // inline 264 | // after 265 | ]; 266 | }` 267 | p := newParserOn(src) 268 | msg := new(Message) 269 | p.next() 270 | if err := msg.parse(p); err != nil { 271 | t.Fatal(err) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /noop_visitor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | var _ Visitor = NoopVisitor{} 27 | 28 | // NoopVisitor is a no-operation visitor that can be used when creating your own visitor that is interested in only one or a few types. 29 | // It implements the Visitor interface. 30 | type NoopVisitor struct{} 31 | 32 | // VisitMessage is part of Visitor interface 33 | func (n NoopVisitor) VisitMessage(m *Message) {} 34 | 35 | // VisitService is part of Visitor interface 36 | func (n NoopVisitor) VisitService(v *Service) {} 37 | 38 | // VisitSyntax is part of Visitor interface 39 | func (n NoopVisitor) VisitSyntax(s *Syntax) {} 40 | 41 | // VisitSyntax is part of Visitor interface 42 | func (n NoopVisitor) VisitEdition(e *Edition) {} 43 | 44 | // VisitPackage is part of Visitor interface 45 | func (n NoopVisitor) VisitPackage(p *Package) {} 46 | 47 | // VisitOption is part of Visitor interface 48 | func (n NoopVisitor) VisitOption(o *Option) {} 49 | 50 | // VisitImport is part of Visitor interface 51 | func (n NoopVisitor) VisitImport(i *Import) {} 52 | 53 | // VisitNormalField is part of Visitor interface 54 | func (n NoopVisitor) VisitNormalField(i *NormalField) {} 55 | 56 | // VisitEnumField is part of Visitor interface 57 | func (n NoopVisitor) VisitEnumField(i *EnumField) {} 58 | 59 | // VisitEnum is part of Visitor interface 60 | func (n NoopVisitor) VisitEnum(e *Enum) {} 61 | 62 | // VisitComment is part of Visitor interface 63 | func (n NoopVisitor) VisitComment(e *Comment) {} 64 | 65 | // VisitOneof is part of Visitor interface 66 | func (n NoopVisitor) VisitOneof(o *Oneof) {} 67 | 68 | // VisitOneofField is part of Visitor interface 69 | func (n NoopVisitor) VisitOneofField(o *OneOfField) {} 70 | 71 | // VisitReserved is part of Visitor interface 72 | func (n NoopVisitor) VisitReserved(r *Reserved) {} 73 | 74 | // VisitRPC is part of Visitor interface 75 | func (n NoopVisitor) VisitRPC(r *RPC) {} 76 | 77 | // VisitMapField is part of Visitor interface 78 | func (n NoopVisitor) VisitMapField(f *MapField) {} 79 | 80 | // VisitGroup is part of Visitor interface 81 | func (n NoopVisitor) VisitGroup(g *Group) {} 82 | 83 | // VisitExtensions is part of Visitor interface 84 | func (n NoopVisitor) VisitExtensions(e *Extensions) {} 85 | -------------------------------------------------------------------------------- /oneof.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Oneof is a field alternate. 31 | type Oneof struct { 32 | Position scanner.Position 33 | Comment *Comment 34 | Name string 35 | Elements []Visitee 36 | Parent Visitee 37 | } 38 | 39 | // addElement is part of elementContainer 40 | func (o *Oneof) addElement(v Visitee) { 41 | v.parent(o) 42 | o.Elements = append(o.Elements, v) 43 | } 44 | 45 | // elements is part of elementContainer 46 | func (o *Oneof) elements() []Visitee { 47 | return o.Elements 48 | } 49 | 50 | // takeLastComment is part of elementContainer 51 | // removes and returns the last element of the list if it is a Comment. 52 | func (o *Oneof) takeLastComment(expectedOnLine int) (last *Comment) { 53 | last, o.Elements = takeLastCommentIfEndsOnLine(o.Elements, expectedOnLine) 54 | return last 55 | } 56 | 57 | // parse expects: 58 | // oneofName "{" { oneofField | emptyStatement } "}" 59 | func (o *Oneof) parse(p *Parser) error { 60 | pos, tok, lit := p.next() 61 | if tok != tIDENT { 62 | if !isKeyword(tok) { 63 | return p.unexpected(lit, "oneof identifier", o) 64 | } 65 | } 66 | o.Name = lit 67 | consumeCommentFor(p, o) 68 | pos, tok, lit = p.next() 69 | if tok != tLEFTCURLY { 70 | return p.unexpected(lit, "oneof opening {", o) 71 | } 72 | for { 73 | pos, tok, lit = p.nextTypeName() 74 | switch tok { 75 | case tCOMMENT: 76 | if com := mergeOrReturnComment(o.elements(), lit, pos); com != nil { // not merged? 77 | o.addElement(com) 78 | } 79 | case tIDENT: 80 | f := newOneOfField() 81 | f.Position = pos 82 | f.Comment, o.Elements = takeLastCommentIfEndsOnLine(o.elements(), pos.Line-1) // TODO call takeLastComment instead? 83 | f.Type = lit 84 | if err := parseFieldAfterType(f.Field, p, f); err != nil { 85 | return err 86 | } 87 | o.addElement(f) 88 | case tGROUP: 89 | g := new(Group) 90 | g.Position = pos 91 | g.Comment, o.Elements = takeLastCommentIfEndsOnLine(o.elements(), pos.Line-1) 92 | if err := g.parse(p); err != nil { 93 | return err 94 | } 95 | o.addElement(g) 96 | case tOPTION: 97 | opt := new(Option) 98 | opt.Position = pos 99 | opt.Comment, o.Elements = takeLastCommentIfEndsOnLine(o.elements(), pos.Line-1) 100 | if err := opt.parse(p); err != nil { 101 | return err 102 | } 103 | o.addElement(opt) 104 | case tSEMICOLON: 105 | maybeScanInlineComment(p, o) 106 | // continue 107 | default: 108 | goto done 109 | } 110 | } 111 | done: 112 | if tok != tRIGHTCURLY { 113 | return p.unexpected(lit, "oneof closing }", o) 114 | } 115 | return nil 116 | } 117 | 118 | // Accept dispatches the call to the visitor. 119 | func (o *Oneof) Accept(v Visitor) { 120 | v.VisitOneof(o) 121 | } 122 | 123 | // Doc is part of Documented 124 | func (o *Oneof) Doc() *Comment { 125 | return o.Comment 126 | } 127 | 128 | // OneOfField is part of Oneof. 129 | type OneOfField struct { 130 | *Field 131 | } 132 | 133 | func newOneOfField() *OneOfField { return &OneOfField{Field: new(Field)} } 134 | 135 | // Accept dispatches the call to the visitor. 136 | func (o *OneOfField) Accept(v Visitor) { 137 | v.VisitOneofField(o) 138 | } 139 | 140 | // Doc is part of Documented 141 | // Note: although Doc() is defined on Field, it must be implemented here as well. 142 | func (o *OneOfField) Doc() *Comment { 143 | return o.Comment 144 | } 145 | 146 | func (o *Oneof) parent(v Visitee) { o.Parent = v } 147 | -------------------------------------------------------------------------------- /oneof_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestOneof(t *testing.T) { 29 | proto := `oneof foo { 30 | // just a name 31 | string name = 4; 32 | SubMessage sub_message = 9 [options=none]; 33 | }` 34 | p := newParserOn(proto) 35 | p.next() // consume first token 36 | o := new(Oneof) 37 | err := o.parse(p) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | if got, want := o.Name, "foo"; got != want { 42 | t.Errorf("got [%v] want [%v]", got, want) 43 | } 44 | if got, want := len(o.Elements), 2; got != want { 45 | t.Fatalf("got [%v] want [%v]", got, want) 46 | } 47 | first := o.Elements[0].(*OneOfField) 48 | if got, want := first.Comment.Message(), " just a name"; got != want { 49 | t.Errorf("got [%v] want [%v]", got, want) 50 | } 51 | if got, want := first.Position.Line, 3; got != want { 52 | t.Errorf("got [%v] want [%v]", got, want) 53 | } 54 | second := o.Elements[1].(*OneOfField) 55 | if got, want := second.Name, "sub_message"; got != want { 56 | t.Errorf("got [%v] want [%v]", got, want) 57 | } 58 | if got, want := second.Type, "SubMessage"; got != want { 59 | t.Errorf("got [%v] want [%v]", got, want) 60 | } 61 | if got, want := second.Sequence, 9; got != want { 62 | t.Errorf("got [%v] want [%v]", got, want) 63 | } 64 | if got, want := second.Position.Line, 4; got != want { 65 | t.Errorf("got [%v] want [%v]", got, want) 66 | } 67 | checkParent(second.Options[0], t) 68 | } 69 | 70 | func TestFieldOneofImported(t *testing.T) { 71 | fieldType := "foo.bar" 72 | proto := `message Value { 73 | oneof value { 74 | ` + fieldType + ` value = 1; 75 | } 76 | }` 77 | p := newParserOn(proto) 78 | def := new(Proto) 79 | err := def.parse(p) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | m := def.Elements[0].(*Message) 84 | if len(m.Elements) != 1 { 85 | t.Errorf("expected one element but got %d", len(m.Elements)) 86 | } 87 | o := m.Elements[0].(*Oneof) 88 | if len(o.Elements) != 1 { 89 | t.Errorf("expected one element but got %d", len(o.Elements)) 90 | } 91 | f := o.Elements[0].(*OneOfField) 92 | if got, want := f.Type, fieldType; got != want { 93 | t.Errorf("got [%v] want [%v]", got, want) 94 | } 95 | if got, want := f.Name, "value"; got != want { 96 | t.Errorf("got [%v] want [%v]", got, want) 97 | } 98 | } 99 | 100 | func TestFieldOneofDotImported(t *testing.T) { 101 | fieldType := ".foo.bar" 102 | proto := `message Value { 103 | oneof value { 104 | ` + fieldType + ` value = 1; 105 | } 106 | }` 107 | p := newParserOn(proto) 108 | def := new(Proto) 109 | err := def.parse(p) 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | m := def.Elements[0].(*Message) 114 | if len(m.Elements) != 1 { 115 | t.Errorf("expected one element but got %d", len(m.Elements)) 116 | } 117 | o := m.Elements[0].(*Oneof) 118 | if len(o.Elements) != 1 { 119 | t.Errorf("expected one element but got %d", len(o.Elements)) 120 | } 121 | f := o.Elements[0].(*OneOfField) 122 | if got, want := f.Type, fieldType; got != want { 123 | t.Errorf("got [%v] want [%v]", got, want) 124 | } 125 | if got, want := f.Name, "value"; got != want { 126 | t.Errorf("got [%v] want [%v]", got, want) 127 | } 128 | } 129 | 130 | func TestOneOfWithOption(t *testing.T) { 131 | src := `oneof AnOneof { 132 | option (oneof_opt1) = -99; 133 | int32 oneof_field = 2; 134 | }` 135 | p := newParserOn(src) 136 | p.next() 137 | oneof := new(Oneof) 138 | err := oneof.parse(p) 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | option := oneof.Elements[0].(*Option) 143 | if got, want := option.Name, "(oneof_opt1)"; got != want { 144 | t.Errorf("got [%v] want [%v]", got, want) 145 | } 146 | checkParent(option, t) 147 | } 148 | 149 | func TestOneofInlineCommentBeforeBody(t *testing.T) { 150 | src := `oneof BarOption // BarOption 151 | // with another line 152 | { 153 | name string = 1; 154 | } 155 | ` 156 | p := newParserOn(src) 157 | oneof := new(Oneof) 158 | p.next() 159 | if err := oneof.parse(p); err != nil { 160 | t.Fatal(err) 161 | } 162 | nestedComment := oneof.Elements[0].(*Comment) 163 | if nestedComment == nil { 164 | t.Fatal("expected comment present") 165 | } 166 | if got, want := len(nestedComment.Lines), 2; got != want { 167 | t.Errorf("got %d want %d lines", got, want) 168 | } 169 | } 170 | 171 | func TestOneOfDocumented(t *testing.T) { 172 | src := `message Value { 173 | // documented 174 | oneof Foo { 175 | int32 oneof_field = 1; 176 | } 177 | }` 178 | p := newParserOn(src) 179 | def := new(Proto) 180 | err := def.parse(p) 181 | if err != nil { 182 | t.Fatal(err) 183 | } 184 | m := def.Elements[0].(*Message) 185 | if len(m.Elements) != 1 { 186 | t.Errorf("expected one element but got %d", len(m.Elements)) 187 | } 188 | o := m.Elements[0].(*Oneof) 189 | if len(o.Elements) != 1 { 190 | t.Errorf("expected one element but got %d", len(o.Elements)) 191 | } 192 | if Documented(o).Doc() == nil { 193 | t.Fatal("doc expected") 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "fmt" 28 | "text/scanner" 29 | ) 30 | 31 | // Option is a protoc compiler option 32 | type Option struct { 33 | Position scanner.Position 34 | Comment *Comment 35 | Name string 36 | Constant Literal 37 | IsEmbedded bool 38 | // AggregatedConstants is DEPRECATED. These Literals are populated into Constant.OrderedMap 39 | AggregatedConstants []*NamedLiteral 40 | InlineComment *Comment 41 | Parent Visitee 42 | } 43 | 44 | // parse reads an Option body 45 | // ( ident | //... | "(" fullIdent ")" ) { "." ident } "=" constant ";" 46 | func (o *Option) parse(p *Parser) error { 47 | consumeOptionComments(o, p) 48 | 49 | if err := o.parseOptionName(p); err != nil { 50 | return err 51 | } 52 | // check for = 53 | pos, tok, lit := p.next() 54 | if tEQUALS != tok { 55 | return p.unexpected(lit, "option value assignment =", o) 56 | } 57 | // parse value 58 | r := p.peekNonWhitespace() 59 | var err error 60 | // values of an option can have illegal escape sequences 61 | // for the standard Go scanner used by this package. 62 | p.ignoreIllegalEscapesWhile(func() { 63 | if r == '{' { 64 | // aggregate 65 | p.next() // consume { 66 | err = o.parseAggregate(p) 67 | } else { 68 | // non aggregate 69 | l := new(Literal) 70 | l.Position = pos 71 | if e := l.parse(p); e != nil { 72 | err = e 73 | } 74 | o.Constant = *l 75 | } 76 | }) 77 | consumeOptionComments(o, p) 78 | return err 79 | } 80 | 81 | // https://protobuf.dev/reference/protobuf/proto3-spec/#option 82 | func (o *Option) parseOptionName(p *Parser) error { 83 | name := "" 84 | for { 85 | pos, tok, lit := p.nextIdent(true) 86 | switch tok { 87 | case tDOT: 88 | name += "." 89 | case tIDENT: 90 | name += lit 91 | case tLEFTPAREN: 92 | // check for dot 93 | dot := "" // none 94 | if p.peekNonWhitespace() == '.' { 95 | p.next() // consume dot 96 | dot = "." 97 | } 98 | _, tok, lit = p.nextFullIdent(true) 99 | if tok != tIDENT { 100 | return p.unexpected(lit, "option name", o) 101 | } 102 | // check for closing parenthesis 103 | _, tok, _ = p.next() 104 | if tok != tRIGHTPAREN { 105 | return p.unexpected(lit, "option full identifier closing )", o) 106 | } 107 | name = fmt.Sprintf("%s(%s%s)", name, dot, lit) 108 | default: 109 | // put it back 110 | p.nextPut(pos, tok, lit) 111 | goto done 112 | } 113 | } 114 | done: 115 | o.Name = name 116 | return nil 117 | } 118 | 119 | // inlineComment is part of commentInliner. 120 | func (o *Option) inlineComment(c *Comment) { 121 | o.InlineComment = c 122 | } 123 | 124 | // Accept dispatches the call to the visitor. 125 | func (o *Option) Accept(v Visitor) { 126 | v.VisitOption(o) 127 | } 128 | 129 | // Doc is part of Documented 130 | func (o *Option) Doc() *Comment { 131 | return o.Comment 132 | } 133 | 134 | // parseAggregate reads options written using aggregate syntax. 135 | // tLEFTCURLY { has been consumed 136 | func (o *Option) parseAggregate(p *Parser) error { 137 | constants, err := parseAggregateConstants(p, o) 138 | literalMap := map[string]*Literal{} 139 | for _, each := range constants { 140 | literalMap[each.Name] = each.Literal 141 | } 142 | o.Constant = Literal{Map: literalMap, OrderedMap: constants, Position: o.Position} 143 | 144 | // reconstruct the old, deprecated field 145 | o.AggregatedConstants = collectAggregatedConstants(literalMap) 146 | return err 147 | } 148 | 149 | func (o *Option) parent(v Visitee) { o.Parent = v } 150 | -------------------------------------------------------------------------------- /option_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "testing" 28 | ) 29 | 30 | func TestOptionCases(t *testing.T) { 31 | for i, each := range []struct { 32 | proto string 33 | name string 34 | strLit string 35 | nonStrLit string 36 | }{{ 37 | `option (full).java_package = "com.example.foo";`, 38 | "(full).java_package", 39 | "com.example.foo", 40 | "", 41 | }, { 42 | `option Bool = true;`, 43 | "Bool", 44 | "", 45 | "true", 46 | }, { 47 | `option Float = -3.14E1;`, 48 | "Float", 49 | "", 50 | "-3.14E1", 51 | }, { 52 | `option (foo_options) = { opt1: 123 opt2: "baz" };`, 53 | "(foo_options)", 54 | "", 55 | "", 56 | }, { 57 | `option foo = []`, 58 | "foo", 59 | "", 60 | "", 61 | }, { 62 | `option optimize_for = SPEED;`, 63 | "optimize_for", 64 | "", 65 | "SPEED", 66 | }, { 67 | "option (my.enum.service.is.like).rpc = 1;", 68 | "(my.enum.service.is.like).rpc", 69 | "", 70 | "1", 71 | }, { 72 | `option (imported.oss.package).action = "literal-double-quotes";`, 73 | "(imported.oss.package).action", 74 | "literal-double-quotes", 75 | "", 76 | }, { 77 | `option (imported.oss.package).action = "key:\"literal-double-quotes-escaped\"";`, 78 | "(imported.oss.package).action", 79 | `key:\"literal-double-quotes-escaped\"`, 80 | "", 81 | }, { 82 | `option (imported.oss.package).action = 'literalsinglequotes';`, 83 | "(imported.oss.package).action", 84 | "literalsinglequotes", 85 | "", 86 | }, { 87 | `option (imported.oss.package).action = 'single-quotes.with/symbols';`, 88 | "(imported.oss.package).action", 89 | "single-quotes.with/symbols", 90 | "", 91 | }, { 92 | `option features.(pb.go).api_level = API_OPAQUE;`, 93 | "features.(pb.go).api_level", 94 | "API_OPAQUE", 95 | "", 96 | }} { 97 | p := newParserOn(each.proto) 98 | pr, err := p.Parse() 99 | if err != nil { 100 | t.Fatal("testcase failed:", i, err) 101 | } 102 | if got, want := len(pr.Elements), 1; got != want { 103 | t.Fatalf("[%d] got [%v] want [%v]", i, got, want) 104 | } 105 | o := pr.Elements[0].(*Option) 106 | if got, want := o.Name, each.name; got != want { 107 | t.Errorf("[%d] got [%v] want [%v]", i, got, want) 108 | } 109 | if len(each.strLit) > 0 { 110 | if got, want := o.Constant.Source, each.strLit; got != want { 111 | t.Errorf("[%d] got [%v] want [%v]", i, got, want) 112 | } 113 | } 114 | if len(each.nonStrLit) > 0 { 115 | if got, want := o.Constant.Source, each.nonStrLit; got != want { 116 | t.Errorf("[%d] got [%v] want [%v]", i, got, want) 117 | } 118 | } 119 | if got, want := o.IsEmbedded, false; got != want { 120 | t.Errorf("[%d] got [%v] want [%v]", i, got, want) 121 | } 122 | } 123 | } 124 | 125 | func TestOptionComments(t *testing.T) { 126 | proto := ` 127 | // comment 128 | option Help = "me"; // inline` 129 | p := newParserOn(proto) 130 | pr, err := p.Parse() 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | o := pr.Elements[0].(*Option) 135 | if got, want := o.IsEmbedded, false; got != want { 136 | t.Errorf("got [%v] want [%v]", got, want) 137 | } 138 | if got, want := o.Comment != nil, true; got != want { 139 | t.Fatalf("got [%v] want [%v]", got, want) 140 | } 141 | if got, want := o.Comment.Lines[0], " comment"; got != want { 142 | t.Fatalf("got [%v] want [%v]", got, want) 143 | } 144 | if got, want := o.InlineComment != nil, true; got != want { 145 | t.Fatalf("got [%v] want [%v]", got, want) 146 | } 147 | if got, want := o.InlineComment.Lines[0], " inline"; got != want { 148 | t.Fatalf("got [%v] want [%v]", got, want) 149 | } 150 | if got, want := o.Position.Line, 3; got != want { 151 | t.Fatalf("got [%v] want [%v]", got, want) 152 | } 153 | if got, want := o.Comment.Position.Line, 2; got != want { 154 | t.Fatalf("got [%v] want [%v]", got, want) 155 | } 156 | if got, want := o.InlineComment.Position.Line, 3; got != want { 157 | t.Fatalf("got [%v] want [%v]", got, want) 158 | } 159 | } 160 | 161 | func TestAggregateSyntax(t *testing.T) { 162 | proto := ` 163 | // usage: 164 | message Bar { 165 | // alternative aggregate syntax (uses TextFormat): 166 | int32 b = 2 [(foo_options) = { 167 | opt1: 123, 168 | opt2: "baz" 169 | }]; 170 | } 171 | ` 172 | p := newParserOn(proto) 173 | pr, err := p.Parse() 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | o := pr.Elements[0].(*Message) 178 | f := o.Elements[0].(*NormalField) 179 | if got, want := len(f.Options), 1; got != want { 180 | t.Fatalf("got [%v] want [%v]", got, want) 181 | } 182 | ac := f.Options[0].Constant.Map 183 | if got, want := len(ac), 2; got != want { 184 | t.Fatalf("got [%v] want [%v]", got, want) 185 | } 186 | if got, want := ac["opt1"].Source, "123"; got != want { 187 | t.Fatalf("got [%v] want [%v]", got, want) 188 | } 189 | if got, want := o.Position.Line, 3; got != want { 190 | t.Fatalf("got [%v] want [%v]", got, want) 191 | } 192 | if got, want := o.Comment.Position.String(), ":2:1"; got != want { 193 | t.Fatalf("got [%v] want [%v]", got, want) 194 | } 195 | if got, want := f.Position.String(), ":5:3"; got != want { 196 | t.Fatalf("got [%v] want [%v]", got, want) 197 | } 198 | if got, want := f.Options[0].Constant.Position.Line, 5; got != want { 199 | t.Fatalf("got [%v] want [%v]", got, want) 200 | } 201 | // check for AggregatedConstants 202 | list := f.Options[0].AggregatedConstants 203 | if got, want := list[0].Source, "123"; got != want { 204 | t.Fatalf("got [%v] want [%v]", got, want) 205 | } 206 | if got, want := list[1].Source, "baz"; got != want { 207 | t.Fatalf("got [%v] want [%v]", got, want) 208 | } 209 | } 210 | 211 | func TestNonPrimitiveOptionComment(t *testing.T) { 212 | proto := ` 213 | // comment 214 | option Help = { string_field: "value" }; // inline` 215 | p := newParserOn(proto) 216 | pr, err := p.Parse() 217 | if err != nil { 218 | t.Fatal(err) 219 | } 220 | o := pr.Elements[0].(*Option) 221 | if got, want := o.Comment != nil, true; got != want { 222 | t.Fatalf("got [%v] want [%v]", got, want) 223 | } 224 | if got, want := o.Comment.Lines[0], " comment"; got != want { 225 | t.Fatalf("got [%v] want [%v]", got, want) 226 | } 227 | if got, want := o.InlineComment != nil, true; got != want { 228 | t.Fatalf("got [%v] want [%v]", got, want) 229 | } 230 | if got, want := o.InlineComment.Lines[0], " inline"; got != want { 231 | t.Fatalf("got [%v] want [%v]", got, want) 232 | } 233 | } 234 | 235 | func TestFieldCustomOptions(t *testing.T) { 236 | proto := `foo.bar lots = 1 [foo={hello:1}, bar=2];` 237 | p := newParserOn(proto) 238 | f := newNormalField() 239 | err := f.parse(p) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | if got, want := f.Type, "foo.bar"; got != want { 244 | t.Errorf("got [%v] want [%v]", got, want) 245 | } 246 | if got, want := f.Name, "lots"; got != want { 247 | t.Errorf("got [%v] want [%v]", got, want) 248 | } 249 | if got, want := len(f.Options), 2; got != want { 250 | t.Fatalf("got [%v] want [%v]", got, want) 251 | } 252 | if got, want := f.Options[0].Name, "foo"; got != want { 253 | t.Errorf("got [%v] want [%v]", got, want) 254 | } 255 | if got, want := f.Options[1].Name, "bar"; got != want { 256 | t.Errorf("got [%v] want [%v]", got, want) 257 | } 258 | if got, want := f.Options[1].Constant.Source, "2"; got != want { 259 | t.Errorf("got [%v] want [%v]", got, want) 260 | } 261 | // check for AggregatedConstants 262 | if got, want := f.Options[0].AggregatedConstants[0].Name, "hello"; got != want { 263 | t.Errorf("got [%v] want [%v]", got, want) 264 | } 265 | if got, want := f.Options[0].AggregatedConstants[0].PrintsColon, true; got != want { 266 | t.Errorf("got [%v] want [%v]", got, want) 267 | } 268 | if got, want := f.Options[0].AggregatedConstants[0].Source, "1"; got != want { 269 | t.Errorf("got [%v] want [%v]", got, want) 270 | } 271 | } 272 | 273 | func TestIgnoreIllegalEscapeCharsInAggregatedConstants(t *testing.T) { 274 | src := `syntax = "proto3"; 275 | message Person { 276 | string name = 3 [(validate.rules).string = { 277 | pattern: "^[^\d\s]+( [^\d\s]+)*$", 278 | max_bytes: 256, 279 | }]; 280 | }` 281 | p := newParserOn(src) 282 | d, err := p.Parse() 283 | if err != nil { 284 | t.Fatal(err) 285 | } 286 | f := d.Elements[1].(*Message).Elements[0].(*NormalField) 287 | if got, want := f.Options[0].Name, "(validate.rules).string"; got != want { 288 | t.Errorf("got [%v] want [%v]", got, want) 289 | } 290 | if got, want := len(f.Options[0].Constant.Map), 2; got != want { 291 | t.Errorf("got [%v] want [%v]", got, want) 292 | } 293 | if got, want := f.Options[0].Constant.Map["pattern"].Source, "^[^\\d\\s]+( [^\\d\\s]+)*$"; got != want { 294 | t.Errorf("got [%v] want [%v]", got, want) 295 | } 296 | if got, want := len(f.Options[0].Constant.OrderedMap), 2; got != want { 297 | t.Errorf("got [%v] want [%v]", got, want) 298 | } 299 | if got, want := f.Options[0].Constant.OrderedMap[0].Name, "pattern"; got != want { 300 | t.Errorf("got [%v] want [%v]", got, want) 301 | } 302 | if got, want := f.Options[0].Constant.OrderedMap[0].Source, "^[^\\d\\s]+( [^\\d\\s]+)*$"; got != want { 303 | t.Errorf("got [%v] want [%v]", got, want) 304 | } 305 | // check for AggregatedConstants 306 | if got, want := f.Options[0].AggregatedConstants[0].Name, "pattern"; got != want { 307 | t.Errorf("got [%v] want [%v]", got, want) 308 | } 309 | if got, want := f.Options[0].AggregatedConstants[0].PrintsColon, true; got != want { 310 | t.Errorf("got [%v] want [%v]", got, want) 311 | } 312 | if got, want := f.Options[0].AggregatedConstants[0].Source, "^[^\\d\\s]+( [^\\d\\s]+)*$"; got != want { 313 | t.Errorf("got [%v] want [%v]", got, want) 314 | } 315 | } 316 | 317 | func TestIgnoreIllegalEscapeCharsInConstant(t *testing.T) { 318 | src := `syntax = "proto2"; 319 | message Person { 320 | optional string cpp_trigraph = 20 [default = "? \? ?? \?? \??? ??/ ?\?-"]; 321 | }` 322 | p := newParserOn(src) 323 | d, err := p.Parse() 324 | if err != nil { 325 | t.Fatal(err) 326 | } 327 | f := d.Elements[1].(*Message).Elements[0].(*NormalField) 328 | if got, want := f.Options[0].Constant.Source, "? \\? ?? \\?? \\??? ??/ ?\\?-"; got != want { 329 | t.Errorf("got [%v] want [%v]", got, want) 330 | } 331 | } 332 | 333 | func TestFieldCustomOptionExtendedIdent(t *testing.T) { 334 | proto := `Type field = 1 [(validate.rules).enum.defined_only = true];` 335 | p := newParserOn(proto) 336 | f := newNormalField() 337 | err := f.parse(p) 338 | if err != nil { 339 | t.Fatal(err) 340 | } 341 | if got, want := f.Options[0].Name, "(validate.rules).enum.defined_only"; got != want { 342 | t.Errorf("got [%v] want [%v]", got, want) 343 | } 344 | } 345 | 346 | // issue #50 347 | func TestNestedAggregateConstants(t *testing.T) { 348 | src := `syntax = "proto3"; 349 | 350 | package baz; 351 | 352 | option (foo.bar) = { 353 | woot: 100 354 | foo { 355 | hello: 200 356 | hello2: 300 357 | bar { 358 | hello3: 400 359 | } 360 | } 361 | };` 362 | p := newParserOn(src) 363 | proto, err := p.Parse() 364 | if err != nil { 365 | t.Error(err) 366 | } 367 | option := proto.Elements[2].(*Option) 368 | if got, want := option.Name, "(foo.bar)"; got != want { 369 | t.Errorf("got [%v] want [%v]", got, want) 370 | } 371 | if got, want := len(option.Constant.Map), 2; got != want { 372 | t.Errorf("got [%v] want [%v]", got, want) 373 | } 374 | m := option.Constant.Map 375 | if got, want := m["woot"].Source, "100"; got != want { 376 | t.Errorf("got [%v] want [%v]", got, want) 377 | } 378 | if got, want := len(m["foo"].Map), 3; got != want { 379 | t.Errorf("got [%v] want [%v]", got, want) 380 | } 381 | m = m["foo"].Map 382 | if got, want := len(m["bar"].Map), 1; got != want { 383 | t.Errorf("got [%v] want [%v]", got, want) 384 | } 385 | if got, want := m["bar"].Map["hello3"].Source, "400"; got != want { 386 | t.Errorf("got [%v] want [%v]", got, want) 387 | } 388 | if got, want := option.Constant.OrderedMap[1].Name, "foo"; got != want { 389 | t.Errorf("got [%v] want [%v]", got, want) 390 | } 391 | if got, want := option.Constant.OrderedMap[1].OrderedMap[2].Name, "bar"; got != want { 392 | t.Errorf("got [%v] want [%v]", got, want) 393 | } 394 | if got, want := option.Constant.OrderedMap[1].OrderedMap[2].OrderedMap[0].Source, "400"; got != want { 395 | t.Errorf("got [%v] want [%v]", got, want) 396 | } 397 | if got, want := len(option.AggregatedConstants), 4; got != want { 398 | t.Errorf("got [%v] want [%v]", got, want) 399 | } 400 | // for _, each := range option.AggregatedConstants { 401 | // t.Logf("%#v=%v\n", each, each.SourceRepresentation()) 402 | // } 403 | } 404 | 405 | // Issue #59 406 | func TestMultiLineOptionAggregateValue(t *testing.T) { 407 | src := `rpc ListTransferLogs(ListTransferLogsRequest) 408 | returns (ListTransferLogsResponse) { 409 | option (google.api.http) = { 410 | get: "/v1/{parent=projects/*/locations/*/transferConfigs/*/runs/*}/" 411 | "transferLogs" 412 | }; 413 | }` 414 | p := newParserOn(src) 415 | rpc := new(RPC) 416 | p.next() 417 | err := rpc.parse(p) 418 | if err != nil { 419 | t.Error(err) 420 | } 421 | get := rpc.Options[0].Constant.Map["get"] 422 | if got, want := get.Source, "/v1/{parent=projects/*/locations/*/transferConfigs/*/runs/*}/transferLogs"; got != want { 423 | t.Errorf("got [%v] want [%v]", got, want) 424 | } 425 | } 426 | 427 | // issue #76 428 | func TestOptionAggregateCanUseKeyword(t *testing.T) { 429 | src := `message User { 430 | string email = 3 [(validate.field) = {required: true}]; 431 | }` 432 | p := newParserOn(src) 433 | _, err := p.Parse() 434 | if err != nil { 435 | t.Error(err) 436 | } 437 | } 438 | 439 | // issue #77 440 | func TestOptionAggregateWithRepeatedValues(t *testing.T) { 441 | src := `message Envelope { 442 | int64 not_in = 15 [(validate.rules).int64 = {not_in: [40, 45]}]; 443 | int64 in = 16 [(validate.rules).int64 = {in: [[1],[2]]}]; 444 | }` 445 | p := newParserOn(src) 446 | def, err := p.Parse() 447 | if err != nil { 448 | t.Error(err) 449 | } 450 | field := def.Elements[0].(*Message).Elements[0].(*NormalField) 451 | notIn := field.Options[0].Constant.Map["not_in"] 452 | if got, want := len(notIn.Array), 2; got != want { 453 | t.Errorf("got [%v] want [%v]", got, want) 454 | } 455 | if got, want := notIn.Array[0].Source, "40"; got != want { 456 | t.Errorf("got [%v] want [%v]", got, want) 457 | } 458 | if got, want := notIn.Array[1].Source, "45"; got != want { 459 | t.Errorf("got [%v] want [%v]", got, want) 460 | } 461 | } 462 | 463 | func TestInvalidOptionAggregateWithRepeatedValues(t *testing.T) { 464 | src := `message Bogus { 465 | int64 a = 1 [a = {not_in: [40 syntax]}]; 466 | }` 467 | p := newParserOn(src) 468 | _, err := p.Parse() 469 | if err == nil { 470 | t.Error("expected syntax error") 471 | } 472 | } 473 | 474 | // issue #79 475 | func TestUseOfSemicolonsInAggregatedConstants(t *testing.T) { 476 | src := `rpc Test(Void) returns (Void) { 477 | option (google.api.http) = { 478 | post: "/api/v1/test"; 479 | body: "*"; // ignored comment 480 | }; 481 | }` 482 | p := newParserOn(src) 483 | rpc := new(RPC) 484 | p.next() 485 | err := rpc.parse(p) 486 | if err != nil { 487 | t.Fatal(err) 488 | } 489 | if got, want := len(rpc.Elements), 1; got != want { 490 | t.Errorf("got [%v] want [%v]", got, want) 491 | } 492 | opt := rpc.Elements[0].(*Option) 493 | if got, want := len(opt.Constant.Map), 2; got != want { 494 | t.Fatalf("got [%v] want [%v]", got, want) 495 | } 496 | // old access to map 497 | if got, want := opt.Constant.Map["body"].Source, "*"; got != want { 498 | t.Errorf("got [%v] want [%v]", got, want) 499 | } 500 | // new access to map 501 | body, ok := opt.Constant.OrderedMap.Get("body") 502 | if !ok { 503 | t.Fatal("expected body key") 504 | } 505 | if got, want := body.Source, "*"; got != want { 506 | t.Errorf("got [%v] want [%v]", got, want) 507 | } 508 | // for _, each := range opt.Constant.OrderedMap { 509 | // t.Log(each) 510 | // } 511 | } 512 | 513 | func TestParseNestedSelectorInAggregatedConstant(t *testing.T) { 514 | src := `rpc Test(Void) returns (Void) { 515 | option (google.api.http) = { 516 | get: "/api/v1/test" 517 | additional_bindings.post: "/api/v1/test" 518 | additional_bindings.body: "*" 519 | }; 520 | }` 521 | p := newParserOn(src) 522 | rpc := new(RPC) 523 | p.next() 524 | err := rpc.parse(p) 525 | if err != nil { 526 | t.Fatal(err) 527 | } 528 | if got, want := rpc.Options[0].Constant.Map["get"].Source, "/api/v1/test"; got != want { 529 | t.Errorf("got [%v] want [%v]", got, want) 530 | } 531 | if got, want := rpc.Options[0].Constant.Map["additional_bindings.post"].Source, "/api/v1/test"; got != want { 532 | t.Errorf("got [%v] want [%v]", got, want) 533 | } 534 | if got, want := rpc.Options[0].AggregatedConstants[2].Name, "additional_bindings.body"; got != want { 535 | t.Errorf("got [%v] want [%v]", got, want) 536 | } 537 | if got, want := rpc.Options[0].AggregatedConstants[2].Source, "*"; got != want { 538 | t.Errorf("got [%v] want [%v]", got, want) 539 | } 540 | } 541 | 542 | func TestParseMultilineStringConstant(t *testing.T) { 543 | src := `message Test { 544 | string description = 3 [ 545 | (common.ui_field_desc) = "Description of the account" 546 | " domain (e.g. Team," 547 | "Name User Account Directory)." 548 | ]; 549 | }` 550 | p := newParserOn(src) 551 | m := new(Message) 552 | p.next() 553 | err := m.parse(p) 554 | if err != nil { 555 | t.Fatal(err) 556 | } 557 | s := m.Elements[0].(*NormalField).Options[0].Constant.Source 558 | if got, want := s, "Description of the account domain (e.g. Team,Name User Account Directory)."; got != want { 559 | t.Errorf("got [%v] want [%v]", got, want) 560 | } 561 | } 562 | 563 | func TestOptionWithRepeatedMessageValues(t *testing.T) { 564 | src := `message Foo { 565 | int64 a = 1 [b = {repeated_message_field: [{hello: 1}, {hello: 2}]}]; 566 | }` 567 | p := newParserOn(src) 568 | def, err := p.Parse() 569 | if err != nil { 570 | t.Errorf("expected no error but got %v", err) 571 | } 572 | opt := def.Elements[0].(*Message).Elements[0].(*NormalField).Options[0] 573 | hello, ok := opt.AggregatedConstants[0].Array[0].OrderedMap.Get("hello") 574 | if !ok { 575 | t.Fail() 576 | } 577 | if got, want := hello.SourceRepresentation(), "1"; got != want { 578 | t.Errorf("got [%v] want [%v]", got, want) 579 | } 580 | } 581 | 582 | func TestOptionWithRepeatedMessageValuesWithArray(t *testing.T) { 583 | src := `message Foo { 584 | int64 a = 1 [ (bar.repeated_field_dep_option) = 585 | { hello: 1, repeated_dep: [ 586 | { hello: 1, repeated_bar: [1, 2] }, 587 | { hello: 3, repeated_bar: [3, 4] } ] } ]; 588 | }` 589 | p := newParserOn(src) 590 | def, err := p.Parse() 591 | if err != nil { 592 | t.Errorf("expected no error but got %v", err) 593 | } 594 | opt := def.Elements[0].(*Message).Elements[0].(*NormalField).Options[0] 595 | hello, ok := opt.Constant.OrderedMap.Get("hello") 596 | if !ok { 597 | t.Fail() 598 | } 599 | if got, want := hello.SourceRepresentation(), "1"; got != want { 600 | t.Errorf("got [%v] want [%v]", got, want) 601 | } 602 | repeatedDep, ok := opt.Constant.OrderedMap.Get("repeated_dep") 603 | if !ok { 604 | t.Fail() 605 | } 606 | if got, want := len(repeatedDep.Array), 2; got != want { 607 | t.Errorf("got [%v] want [%v]", got, want) 608 | } 609 | hello, ok = repeatedDep.Array[0].OrderedMap.Get("hello") 610 | if !ok { 611 | t.Fail() 612 | } 613 | if got, want := hello.SourceRepresentation(), "1"; got != want { 614 | t.Errorf("got [%v] want [%v]", got, want) 615 | } 616 | onetwo, ok := repeatedDep.Array[0].OrderedMap.Get("repeated_bar") 617 | if !ok { 618 | t.Fail() 619 | } 620 | if got, want := onetwo.Array[0].Source, "1"; got != want { 621 | t.Errorf("got [%v] want [%v]", got, want) 622 | } 623 | if got, want := onetwo.Array[1].Source, "2"; got != want { 624 | t.Errorf("got [%v] want [%v]", got, want) 625 | } 626 | } 627 | 628 | // https://github.com/emicklei/proto/issues/99 629 | func TestFieldCustomOptionLeadingDot(t *testing.T) { 630 | proto := `string app_entity_id = 4 [(.common.v1.some_custom_option) = { opt1: true opt2: false }];` 631 | p := newParserOn(proto) 632 | f := newNormalField() 633 | err := f.parse(p) 634 | if err != nil { 635 | t.Fatal(err) 636 | } 637 | if got, want := f.Type, "string"; got != want { 638 | t.Errorf("got [%v] want [%v]", got, want) 639 | } 640 | o := f.Options[0] 641 | if got, want := o.Name, "(.common.v1.some_custom_option)"; got != want { 642 | t.Fatalf("got [%v] want [%v]", got, want) 643 | } 644 | } 645 | 646 | // https://github.com/emicklei/proto/issues/106 647 | func TestEmptyArrayInOptionStructure(t *testing.T) { 648 | src := ` 649 | option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { 650 | json_schema : { 651 | title : "Frob a request" 652 | description : "blah blah blah" 653 | required : [ ] 654 | optional:["this"] 655 | } 656 | }; 657 | ` 658 | p := newParserOn(src) 659 | p.next() 660 | o := new(Option) 661 | if err := o.parse(p); err != nil { 662 | t.Fatal("testcase parse failed:", err) 663 | } 664 | s, ok := o.Constant.OrderedMap.Get("json_schema") 665 | if !ok { 666 | t.Fatal("expected json_schema literal") 667 | } 668 | // none 669 | a, ok := s.OrderedMap.Get("required") 670 | if !ok { 671 | t.Fatal("expected required literal") 672 | } 673 | if len(a.Array) != 0 { 674 | t.Fatal("expecting empty array") 675 | } 676 | // one 677 | a, ok = s.OrderedMap.Get("optional") 678 | if !ok { 679 | t.Fatal("expected required literal") 680 | } 681 | if len(a.Array) != 1 { 682 | t.Fatal("expecting one size array") 683 | } 684 | if got, want := a.Array[0].Source, "this"; got != want { 685 | t.Fatalf("got [%s] want [%s]", got, want) 686 | } 687 | } 688 | 689 | // https://github.com/emicklei/proto/issues/107 690 | func TestQuoteNotDroppedInOption(t *testing.T) { 691 | src := `string name = 1 [ quote = '<="foo"' ];` 692 | f := newNormalField() 693 | if err := f.parse(newParserOn(src)); err != nil { 694 | t.Fatal(err) 695 | } 696 | sr := f.Options[0].Constant.SourceRepresentation() 697 | if got, want := sr, `'<="foo"'`; got != want { 698 | t.Errorf("got [%s] want [%s]", got, want) 699 | } 700 | } 701 | 702 | func TestWhatYouTypeIsWhatYouGetOptionValue(t *testing.T) { 703 | src := `string n = 1 [ quote = 'm"\"/"' ];` 704 | f := newNormalField() 705 | if err := f.parse(newParserOn(src)); err != nil { 706 | t.Fatal(err) 707 | } 708 | sr := f.Options[0].Constant.SourceRepresentation() 709 | if got, want := sr, `'m"\"/"'`; got != want { 710 | t.Errorf("got [%s] want [%s]", got, want) 711 | } 712 | } 713 | 714 | func TestLiteralNoQuoteRuneSet(t *testing.T) { 715 | l := Literal{ 716 | Source: "foo", 717 | IsString: true, 718 | } 719 | if got, want := l.SourceRepresentation(), "\"foo\""; got != want { 720 | t.Errorf("got [%s] want [%s]", got, want) 721 | } 722 | } 723 | 724 | func TestStringValuesParsedAsNumbers(t *testing.T) { 725 | src := `VAL0 = 0 [(enum_opt) = '09'];` 726 | f := new(EnumField) 727 | if err := f.parse(newParserOn(src)); err != nil { 728 | t.Fatal(err) 729 | } 730 | if got, want := f.ValueOption.Constant.Source, "09"; got != want { 731 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 732 | } 733 | } 734 | 735 | func TestCommentInsideArray(t *testing.T) { 736 | src := `option test = { 737 | scope_rules : [ 738 | // A comment 739 | // Another comment 740 | {has : [ 741 | // comment on test 742 | "test" 743 | ]} 744 | ] 745 | }; 746 | ` 747 | opt, err := newParserOn(src).Parse() 748 | if err != nil { 749 | t.Fatal(err) 750 | } 751 | opt2 := opt.Elements[0].(*Option) 752 | scp, _ := opt2.Constant.OrderedMap.Get("scope_rules") 753 | elem0 := scp.Array[0] 754 | t.Log("comment:", elem0.Comment.Lines) 755 | has, _ := elem0.OrderedMap.Get("has") 756 | if got, want := has.Array[0].Source, "test"; got != want { 757 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 758 | } 759 | t.Log("comment:", has.Array[0].Comment.Lines) 760 | } 761 | 762 | func TestParseBracedFullIdent(t *testing.T) { 763 | src := `features.(pb.go).api_level = API_OPAQUE;` 764 | p := newParserOn(src) 765 | o := new(Option) 766 | o.parseOptionName(p) 767 | if got, want := o.Name, "features.(pb.go).api_level"; got != want { 768 | t.Errorf("got [%[1]v:%[1]T] want [%[2]v:%[2]T]", got, want) 769 | } 770 | } 771 | func TestParseBracedFullIdentWithLeadingDot(t *testing.T) { 772 | src := `some.(.like).it.(.hot) = API_OPAQUE;` 773 | p := newParserOn(src) 774 | o := new(Option) 775 | o.parseOptionName(p) 776 | if got, want := o.Name, "some.(.like).it.(.hot)"; got != want { 777 | t.Errorf("got [%[1]v:%[1]T] want [%[2]v:%[2]T]", got, want) 778 | } 779 | } 780 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "text/scanner" 27 | 28 | // Package specifies the namespace for all proto elements. 29 | type Package struct { 30 | Position scanner.Position 31 | Comment *Comment 32 | Name string 33 | InlineComment *Comment 34 | Parent Visitee 35 | } 36 | 37 | // Doc is part of Documented 38 | func (p *Package) Doc() *Comment { 39 | return p.Comment 40 | } 41 | 42 | func (p *Package) parse(pr *Parser) error { 43 | _, tok, lit := pr.nextIdent(true) 44 | if tIDENT != tok { 45 | if !isKeyword(tok) { 46 | return pr.unexpected(lit, "package identifier", p) 47 | } 48 | } 49 | p.Name = lit 50 | return nil 51 | } 52 | 53 | // Accept dispatches the call to the visitor. 54 | func (p *Package) Accept(v Visitor) { 55 | v.VisitPackage(p) 56 | } 57 | 58 | // inlineComment is part of commentInliner. 59 | func (p *Package) inlineComment(c *Comment) { 60 | p.InlineComment = c 61 | } 62 | 63 | func (p *Package) parent(v Visitee) { p.Parent = v } 64 | -------------------------------------------------------------------------------- /package_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "testing" 28 | ) 29 | 30 | func TestPackageParseWithReservedPrefix(t *testing.T) { 31 | want := "rpc.enum.oneof" 32 | ident := " " + want + ";" 33 | 34 | pkg := new(Package) 35 | p := newParserOn(ident) 36 | if err := pkg.parse(p); err != nil { 37 | t.Error(err) 38 | } 39 | 40 | if pkg.Name != want { 41 | t.Errorf("got %q want %q", pkg.Name, want) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /parent_accessor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | func getParent(child Visitee) Visitee { 27 | if child == nil { 28 | return nil 29 | } 30 | pa := new(parentAccessor) 31 | child.Accept(pa) 32 | return pa.parent 33 | } 34 | 35 | type parentAccessor struct { 36 | parent Visitee 37 | } 38 | 39 | func (p *parentAccessor) VisitMessage(m *Message) { 40 | p.parent = m.Parent 41 | } 42 | func (p *parentAccessor) VisitService(v *Service) { 43 | p.parent = v.Parent 44 | } 45 | func (p *parentAccessor) VisitSyntax(s *Syntax) { 46 | p.parent = s.Parent 47 | } 48 | func (p *parentAccessor) VisitPackage(pkg *Package) { 49 | p.parent = pkg.Parent 50 | } 51 | func (p *parentAccessor) VisitOption(o *Option) { 52 | p.parent = o.Parent 53 | } 54 | func (p *parentAccessor) VisitImport(i *Import) { 55 | p.parent = i.Parent 56 | } 57 | func (p *parentAccessor) VisitNormalField(i *NormalField) { 58 | p.parent = i.Parent 59 | } 60 | func (p *parentAccessor) VisitEnumField(i *EnumField) { 61 | p.parent = i.Parent 62 | } 63 | func (p *parentAccessor) VisitEnum(e *Enum) { 64 | p.parent = e.Parent 65 | } 66 | func (p *parentAccessor) VisitComment(e *Comment) {} 67 | func (p *parentAccessor) VisitOneof(o *Oneof) { 68 | p.parent = o.Parent 69 | } 70 | func (p *parentAccessor) VisitOneofField(o *OneOfField) { 71 | p.parent = o.Parent 72 | } 73 | func (p *parentAccessor) VisitReserved(rs *Reserved) { 74 | p.parent = rs.Parent 75 | } 76 | func (p *parentAccessor) VisitRPC(rpc *RPC) { 77 | p.parent = rpc.Parent 78 | } 79 | func (p *parentAccessor) VisitMapField(f *MapField) { 80 | p.parent = f.Parent 81 | } 82 | func (p *parentAccessor) VisitGroup(g *Group) { 83 | p.parent = g.Parent 84 | } 85 | func (p *parentAccessor) VisitExtensions(e *Extensions) { 86 | p.parent = e.Parent 87 | } 88 | func (p *parentAccessor) VisitEdition(e *Edition) { 89 | p.parent = e.Parent 90 | } 91 | func (p *parentAccessor) VisitProto(*Proto) {} 92 | -------------------------------------------------------------------------------- /parent_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Ernest Micklei 2 | // 3 | // # MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | package proto 24 | 25 | import ( 26 | "fmt" 27 | "testing" 28 | ) 29 | 30 | type parentChecker struct { 31 | errors []error 32 | } 33 | 34 | func checkParent(v Visitee, t *testing.T) { 35 | pc := new(parentChecker) 36 | v.Accept(pc) 37 | if len(pc.errors) == 0 { 38 | return 39 | } 40 | for _, each := range pc.errors { 41 | t.Error(each) 42 | } 43 | } 44 | 45 | func (pc *parentChecker) checkAll(list []Visitee, parent Visitee) { 46 | for _, each := range list { 47 | if _, ok := each.(*Comment); ok { 48 | continue 49 | } 50 | if got, want := getParent(each), parent; got != want { 51 | pc.errors = append(pc.errors, fmt.Errorf("%T has wrong parent set, got %v want %v", each, got, want)) 52 | } 53 | each.Accept(pc) 54 | } 55 | } 56 | func (pc *parentChecker) check(astType, astName string, parent Visitee) { 57 | if parent == nil { 58 | pc.errors = append(pc.errors, fmt.Errorf("%s %s has no parent set", astType, astName)) 59 | } 60 | } 61 | 62 | func (pc *parentChecker) VisitProto(p *Proto) { 63 | pc.checkAll(p.Elements, p) 64 | } 65 | func (pc *parentChecker) VisitMessage(m *Message) { 66 | pc.check("Message", m.Name, m.Parent) 67 | pc.checkAll(m.Elements, m) 68 | } 69 | func (pc *parentChecker) VisitService(v *Service) { 70 | pc.check("Service", v.Name, v.Parent) 71 | pc.checkAll(v.Elements, v) 72 | } 73 | func (pc *parentChecker) VisitSyntax(s *Syntax) { 74 | pc.check("Syntax", s.Value, s.Parent) 75 | } 76 | func (pc *parentChecker) VisitPackage(p *Package) { 77 | pc.check("Package", p.Name, p.Parent) 78 | } 79 | func (pc *parentChecker) VisitOption(o *Option) { 80 | pc.check("Option", o.Name, o.Parent) 81 | } 82 | func (pc *parentChecker) VisitImport(i *Import) { 83 | pc.check("Import", i.Filename, i.Parent) 84 | } 85 | func (pc *parentChecker) VisitNormalField(i *NormalField) { 86 | pc.check("NormalField", i.Name, i.Parent) 87 | } 88 | func (pc *parentChecker) VisitEnumField(i *EnumField) { 89 | pc.check("EnumField", i.Name, i.Parent) 90 | } 91 | func (pc *parentChecker) VisitEnum(e *Enum) { 92 | pc.check("Enum", e.Name, e.Parent) 93 | pc.checkAll(e.Elements, e) 94 | } 95 | func (pc *parentChecker) VisitComment(e *Comment) {} 96 | func (pc *parentChecker) VisitOneof(o *Oneof) { 97 | pc.check("Oneof", o.Name, o.Parent) 98 | pc.checkAll(o.Elements, o) 99 | } 100 | func (pc *parentChecker) VisitOneofField(o *OneOfField) { 101 | pc.check("OneOfField", o.Name, o.Parent) 102 | } 103 | func (pc *parentChecker) VisitReserved(r *Reserved) { 104 | pc.check("Reserved", "", r.Parent) 105 | } 106 | func (pc *parentChecker) VisitRPC(r *RPC) { 107 | pc.check("RPC", r.Name, r.Parent) 108 | //pc.checkAll(r.Options, r) 109 | for _, each := range r.Options { 110 | pc.check("Option", each.Name, r) 111 | } 112 | } 113 | func (pc *parentChecker) VisitMapField(f *MapField) { 114 | pc.check("MapField", f.Name, f.Parent) 115 | } 116 | 117 | // proto2 118 | func (pc *parentChecker) VisitGroup(g *Group) { 119 | pc.check("Group", g.Name, g.Parent) 120 | pc.checkAll(g.Elements, g) 121 | } 122 | func (pc *parentChecker) VisitExtensions(e *Extensions) { 123 | pc.check("Extensions", "", e.Parent) 124 | } 125 | 126 | // edition (proto3+) 127 | func (pc *parentChecker) VisitEdition(e *Edition) { 128 | pc.check("Edition", "", e.Parent) 129 | } 130 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "bytes" 28 | "errors" 29 | "fmt" 30 | "io" 31 | "runtime" 32 | "strconv" 33 | "strings" 34 | "text/scanner" 35 | ) 36 | 37 | // Parser represents a parser. 38 | type Parser struct { 39 | debug bool 40 | scanner *scanner.Scanner 41 | buf *nextValues 42 | scannerErrors []error 43 | } 44 | 45 | // nextValues is to capture the result of next() 46 | type nextValues struct { 47 | pos scanner.Position 48 | tok token 49 | lit string 50 | } 51 | 52 | // NewParser returns a new instance of Parser. 53 | func NewParser(r io.Reader) *Parser { 54 | s := new(scanner.Scanner) 55 | s.Init(r) 56 | s.Mode = scanner.ScanIdents | scanner.ScanFloats | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanComments 57 | p := &Parser{scanner: s} 58 | s.Error = p.handleScanError 59 | return p 60 | } 61 | 62 | // handleScanError is called from the underlying Scanner 63 | func (p *Parser) handleScanError(s *scanner.Scanner, msg string) { 64 | p.scannerErrors = append(p.scannerErrors, 65 | fmt.Errorf("go scanner error at %v = %v", s.Position, msg)) 66 | } 67 | 68 | // ignoreIllegalEscapesWhile is called for scanning constants of an option. 69 | // Such content can have a syntax that is not acceptable by the Go scanner. 70 | // This temporary installs a handler that ignores only one type of error: illegal char escape 71 | func (p *Parser) ignoreIllegalEscapesWhile(block func()) { 72 | // during block call change error handler 73 | p.scanner.Error = func(s *scanner.Scanner, msg string) { 74 | // this catches both "illegal char escape" <= go1.12 and "invalid char escape" go1.13 75 | if strings.Contains(msg, "char escape") { // too bad there is no constant for this in scanner pkg 76 | return 77 | } 78 | p.handleScanError(s, msg) 79 | } 80 | block() 81 | // restore 82 | p.scanner.Error = p.handleScanError 83 | } 84 | 85 | // Parse parses a proto definition. May return a parse or scanner error. 86 | func (p *Parser) Parse() (*Proto, error) { 87 | proto := new(Proto) 88 | if p.scanner.Filename != "" { 89 | proto.Filename = p.scanner.Filename 90 | } 91 | parseError := proto.parse(p) 92 | // see if it was a scanner error 93 | if len(p.scannerErrors) > 0 { 94 | buf := new(bytes.Buffer) 95 | for _, each := range p.scannerErrors { 96 | fmt.Fprintln(buf, each) 97 | } 98 | return proto, errors.New(buf.String()) 99 | } 100 | return proto, parseError 101 | } 102 | 103 | // Filename is for reporting. Optional. 104 | func (p *Parser) Filename(f string) { 105 | p.scanner.Filename = f 106 | } 107 | 108 | const stringWithSingleQuote = "'" 109 | 110 | // next returns the next token using the scanner or drain the buffer. 111 | func (p *Parser) next() (pos scanner.Position, tok token, lit string) { 112 | if p.buf != nil { 113 | // consume buf 114 | vals := *p.buf 115 | p.buf = nil 116 | return vals.pos, vals.tok, vals.lit 117 | } 118 | ch := p.scanner.Scan() 119 | if ch == scanner.EOF { 120 | return p.scanner.Position, tEOF, "" 121 | } 122 | lit = p.scanner.TokenText() 123 | // single quote needs additional scanning 124 | if stringWithSingleQuote == lit { 125 | return p.nextSingleQuotedString() 126 | } 127 | return p.scanner.Position, asToken(lit), lit 128 | } 129 | 130 | // pre: first single quote has been read 131 | func (p *Parser) nextSingleQuotedString() (pos scanner.Position, tok token, lit string) { 132 | var ch rune 133 | p.ignoreErrorsWhile(func() { ch = p.scanner.Scan() }) 134 | if ch == scanner.EOF { 135 | return p.scanner.Position, tEOF, "" 136 | } 137 | // string inside single quote 138 | lit = p.scanner.TokenText() 139 | if stringWithSingleQuote == lit { 140 | // empty single quoted string 141 | return p.scanner.Position, tIDENT, "''" 142 | } 143 | 144 | // scan for partial tokens until actual closing single-quote(') token 145 | for { 146 | p.ignoreErrorsWhile(func() { ch = p.scanner.Scan() }) 147 | 148 | if ch == scanner.EOF { 149 | return p.scanner.Position, tEOF, "" 150 | } 151 | 152 | partial := p.scanner.TokenText() 153 | if partial == "'" { 154 | break 155 | } 156 | lit += partial 157 | } 158 | // end quote expected 159 | if stringWithSingleQuote != p.scanner.TokenText() { 160 | p.unexpected(lit, "'", p) 161 | } 162 | return p.scanner.Position, tIDENT, fmt.Sprintf("'%s'", lit) 163 | } 164 | 165 | func (p *Parser) ignoreErrorsWhile(block func()) { 166 | // during block call change error handler which ignores it all 167 | p.scanner.Error = func(s *scanner.Scanner, msg string) { return } 168 | block() 169 | // restore 170 | p.scanner.Error = p.handleScanError 171 | } 172 | 173 | // nextPut sets the buffer 174 | func (p *Parser) nextPut(pos scanner.Position, tok token, lit string) { 175 | p.buf = &nextValues{pos, tok, lit} 176 | } 177 | 178 | func (p *Parser) unexpected(found, expected string, obj interface{}) error { 179 | debug := "" 180 | if p.debug { 181 | _, file, line, _ := runtime.Caller(1) 182 | debug = fmt.Sprintf(" at %s:%d (with %#v)", file, line, obj) 183 | } 184 | return fmt.Errorf("%v: found %q but expected [%s]%s", p.scanner.Position, found, expected, debug) 185 | } 186 | 187 | func (p *Parser) nextInteger() (i int, err error) { 188 | _, tok, lit := p.next() 189 | if "-" == lit { 190 | i, err = p.nextInteger() 191 | return i * -1, err 192 | } 193 | if tok != tNUMBER { 194 | return 0, errors.New("non integer") 195 | } 196 | if strings.HasPrefix(lit, "0x") || strings.HasPrefix(lit, "0X") { 197 | // hex decode 198 | i64, err := strconv.ParseInt(lit, 0, 64) 199 | return int(i64), err 200 | } 201 | i, err = strconv.Atoi(lit) 202 | return 203 | } 204 | 205 | // nextIdentifier consumes tokens which may have one or more dot separators (namespaced idents). 206 | func (p *Parser) nextIdentifier() (pos scanner.Position, tok token, lit string) { 207 | pos, tok, lit = p.nextIdent(false) 208 | if tDOT == tok { 209 | // leading dot allowed 210 | pos, tok, lit = p.nextIdent(false) 211 | lit = "." + lit 212 | } 213 | return 214 | } 215 | 216 | func (p *Parser) nextMessageLiteralFieldName() (pos scanner.Position, tok token, lit string) { 217 | pos, tok, lit = p.nextIdent(true) 218 | if tok == tLEFTSQUARE { 219 | pos, tok, lit = p.nextIdent(true) 220 | _, _, _ = p.next() // consume right square 221 | } 222 | return 223 | } 224 | 225 | // nextTypeName implements the Packages and Name Resolution for finding the name of the type. 226 | // Valid examples: 227 | // .google.protobuf.Empty 228 | // stream T must return tSTREAM 229 | // optional int32 must return tOPTIONAL 230 | // Bogus must return Bogus 231 | func (p *Parser) nextTypeName() (pos scanner.Position, tok token, lit string) { 232 | pos, tok, lit = p.next() 233 | startPos := pos 234 | fullLit := lit 235 | // leading dot allowed 236 | if tDOT == tok { 237 | pos, tok, lit = p.next() 238 | fullLit = fmt.Sprintf(".%s", lit) 239 | } 240 | // type can be namespaced more 241 | for { 242 | r := p.peekNonWhitespace() 243 | if '.' != r { 244 | break 245 | } 246 | p.next() // consume dot 247 | pos, tok, lit = p.next() 248 | fullLit = fmt.Sprintf("%s.%s", fullLit, lit) 249 | tok = tIDENT 250 | } 251 | return startPos, tok, fullLit 252 | } 253 | 254 | func (p *Parser) nextIdent(keywordStartAllowed bool) (pos scanner.Position, tok token, lit string) { 255 | pos, tok, lit = p.next() 256 | if tIDENT != tok { 257 | // can be keyword 258 | if !(isKeyword(tok) && keywordStartAllowed) { 259 | return 260 | } 261 | // proceed with keyword as first literal 262 | } 263 | startPos := pos 264 | fullLit := lit 265 | // see if identifier is namespaced 266 | for { 267 | r := p.peekNonWhitespace() 268 | if r != '.' { 269 | break 270 | } 271 | p.next() // consume dot 272 | fullLit += "." 273 | pos, tok, lit := p.next() 274 | if tIDENT != tok && !isKeyword(tok) { 275 | p.nextPut(pos, tok, lit) 276 | break 277 | } 278 | fullLit += lit 279 | } 280 | return startPos, tIDENT, fullLit 281 | } 282 | 283 | func (p *Parser) peekNonWhitespace() rune { 284 | r := p.scanner.Peek() 285 | if r == scanner.EOF { 286 | return r 287 | } 288 | if isWhitespace(r) { 289 | // consume it 290 | p.scanner.Next() 291 | return p.peekNonWhitespace() 292 | } 293 | return r 294 | } 295 | 296 | // https://protobuf.dev/reference/protobuf/proto3-spec/ 297 | func (p *Parser) nextFullIdent(keywordStartAllowed bool) (pos scanner.Position, tok token, lit string) { 298 | pos, tok, lit = p.next() 299 | if tIDENT != tok { 300 | // can be keyword 301 | if !(isKeyword(tok) && keywordStartAllowed) { 302 | return 303 | } 304 | // proceed with keyword as first literal 305 | } 306 | fullIdent := lit 307 | for { 308 | r := p.peekNonWhitespace() 309 | if r != '.' { 310 | break 311 | } 312 | p.next() // consume dot 313 | pos, tok, lit = p.nextFullIdent(true) 314 | if tok != tIDENT { 315 | p.nextPut(pos, tok, lit) 316 | break 317 | } 318 | fullIdent = fmt.Sprintf("%s.%s", fullIdent, lit) 319 | } 320 | return pos, tIDENT, fullIdent 321 | } 322 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "strings" 28 | "testing" 29 | ) 30 | 31 | func TestParseComment(t *testing.T) { 32 | proto := ` 33 | // first 34 | // second 35 | 36 | /* 37 | ctyle 38 | multi 39 | line 40 | */ 41 | 42 | // cpp style single line // 43 | 44 | message test{} 45 | ` 46 | p := newParserOn(proto) 47 | pr, err := p.Parse() 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | if got, want := len(collect(pr).Comments()), 3; got != want { 53 | t.Errorf("got [%v] want [%v]", got, want) 54 | } 55 | } 56 | 57 | func newParserOn(def string) *Parser { 58 | p := NewParser(strings.NewReader(def)) 59 | p.debug = true 60 | return p 61 | } 62 | 63 | func TestScanIgnoreWhitespace_Digits(t *testing.T) { 64 | p := newParserOn(" 1234 ") 65 | _, _, lit := p.next() 66 | if got, want := lit, "1234"; got != want { 67 | t.Errorf("got [%v] want [%v]", got, want) 68 | } 69 | } 70 | 71 | func TestScanIgnoreWhitespace_Minus(t *testing.T) { 72 | p := newParserOn(" -1234") 73 | _, _, lit := p.next() 74 | if got, want := lit, "-"; got != want { 75 | t.Errorf("got [%v] want [%v]", got, want) 76 | } 77 | } 78 | 79 | func TestNextIdentifier(t *testing.T) { 80 | ident := " aap.noot.mies " 81 | p := newParserOn(ident) 82 | _, tok, lit := p.nextIdentifier() 83 | if got, want := tok, tIDENT; got != want { 84 | t.Errorf("got [%v] want [%v]", got, want) 85 | } 86 | if got, want := lit, strings.TrimSpace(ident); got != want { 87 | t.Errorf("got [%v] want [%v]", got, want) 88 | } 89 | } 90 | 91 | func TestNextIdentifierWithKeyword(t *testing.T) { 92 | ident := " aap.rpc.mies.enum =" 93 | p := newParserOn(ident) 94 | _, tok, lit := p.nextIdentifier() 95 | if got, want := tok, tIDENT; got != want { 96 | t.Errorf("got [%v] want [%v]", got, want) 97 | } 98 | if got, want := lit, "aap.rpc.mies.enum"; got != want { 99 | t.Errorf("got [%v] want [%v]", got, want) 100 | } 101 | _, tok, _ = p.next() 102 | if got, want := tok, tEQUALS; got != want { 103 | t.Errorf("got [%v] want [%v]", got, want) 104 | } 105 | } 106 | 107 | func TestNextTypeNameWithLeadingKeyword(t *testing.T) { 108 | ident := " service.me.now" 109 | p := newParserOn(ident) 110 | _, tok, lit := p.nextTypeName() 111 | if got, want := tok, tIDENT; got != want { 112 | t.Errorf("got [%v] want [%v]", got, want) 113 | } 114 | if got, want := lit, "service.me.now"; got != want { 115 | t.Errorf("got [%v] want [%v]", got, want) 116 | } 117 | } 118 | 119 | func TestNextIdentifierNoIdent(t *testing.T) { 120 | ident := "(" 121 | p := newParserOn(ident) 122 | _, tok, lit := p.nextIdentifier() 123 | if got, want := tok, tLEFTPAREN; got != want { 124 | t.Errorf("got [%v] want [%v]", got, want) 125 | } 126 | if got, want := lit, "("; got != want { 127 | t.Errorf("got [%v] want [%v]", got, want) 128 | } 129 | } 130 | 131 | // https://github.com/google/protobuf/issues/4726 132 | func TestProtobufIssue4726(t *testing.T) { 133 | src := `syntax = "proto3"; 134 | 135 | service SomeService { 136 | rpc SomeMethod (Whatever) returns (Whatever) { 137 | option (google.api.http) = { 138 | delete : "/some/url" 139 | additional_bindings { 140 | delete: "/another/url" 141 | } 142 | }; 143 | } 144 | }` 145 | p := newParserOn(src) 146 | _, err := p.Parse() 147 | if err != nil { 148 | t.Error(err) 149 | } 150 | } 151 | 152 | func TestProtoIssue92(t *testing.T) { 153 | src := `syntax = "proto3"; 154 | 155 | package test; 156 | 157 | message Foo { 158 | .game.Resource one = 1 [deprecated = true]; 159 | repeated .game.sub.Resource two = 2; 160 | map three = 3; 161 | }` 162 | p := newParserOn(src) 163 | _, err := p.Parse() 164 | if err != nil { 165 | t.Error(err) 166 | } 167 | } 168 | 169 | func TestParseSingleQuotesStrings(t *testing.T) { 170 | p := newParserOn(` 'bohemian','' `) 171 | _, _, lit := p.next() 172 | if got, want := lit, "'bohemian'"; got != want { 173 | t.Errorf("got [%v] want [%v]", got, want) 174 | } 175 | _, tok, _ := p.next() 176 | if got, want := tok, tCOMMA; got != want { 177 | t.Errorf("got [%v] want [%v]", got, want) 178 | } 179 | _, _, lit = p.next() 180 | if got, want := lit, "''"; got != want { 181 | t.Errorf("got [%v] want [%v]", got, want) 182 | } 183 | } 184 | 185 | func TestProtoIssue132(t *testing.T) { 186 | src := `syntax = "proto3"; 187 | package tutorial; 188 | message Person { 189 | string name = 1; 190 | int32 id = 0x2; // Unique ID number for this person. 191 | string email = 0X3; // parser.Parse err :8:18: found "=" but expected [field sequence number] 192 | }` 193 | p := newParserOn(src) 194 | _, err := p.Parse() 195 | if err != nil { 196 | t.Error(err) 197 | } 198 | } 199 | 200 | func TestReservedNegativeRanges(t *testing.T) { 201 | r := new(Reserved) 202 | p := newParserOn(`reserved -1;`) 203 | _, tok, _ := p.next() 204 | if tRESERVED != tok { 205 | t.Fail() 206 | } 207 | err := r.parse(p) 208 | if err != nil { 209 | t.Fatal(err) 210 | } 211 | if got, want := r.Ranges[0].SourceRepresentation(), "-1"; got != want { 212 | t.Fatalf("got [%v] want [%v]", got, want) // reserved_test.go:59: got [1] want [-1] 213 | } 214 | } 215 | 216 | func TestParseNegativeEnum(t *testing.T) { 217 | const def = ` 218 | syntax = "proto3"; 219 | package example; 220 | 221 | enum Value { 222 | ZERO = 0; 223 | reserved -2, -1; 224 | }` 225 | 226 | p := NewParser(strings.NewReader(def)) 227 | _, err := p.Parse() 228 | if err != nil { 229 | t.Fatal(err) // :7:16: found "-" but expected [range integer] 230 | } 231 | } 232 | 233 | func TestParseInfMessage(t *testing.T) { 234 | const def = ` 235 | message Inf { 236 | string field = 1; 237 | } 238 | message NaN { 239 | string field = 1; 240 | } 241 | 242 | message Infinity { 243 | string field = 1; 244 | } 245 | message ExampelMessage { 246 | Inf inf_field = 1; 247 | NaN nan_field = 2; 248 | Infinity infinity_field = 3; 249 | } 250 | ` 251 | 252 | p := NewParser(strings.NewReader(def)) 253 | _, err := p.Parse() 254 | if err != nil { 255 | t.Fatal(err) // :7:16: found "-" but expected [range integer] 256 | } 257 | } 258 | 259 | func TestFullIdent(t *testing.T) { 260 | for _, tc := range []struct { 261 | src string 262 | tok token 263 | }{ 264 | {"i", tIDENT}, 265 | {"ident12_", tIDENT}, 266 | {"ident12_ident42.Ident01_Ident2", tIDENT}, 267 | {"enum", tENUM}, 268 | {"enum_enum", tIDENT}, 269 | } { 270 | p := newParserOn(tc.src) 271 | _, tok, lit := p.nextFullIdent(false) 272 | if got, want := tok, tc.tok; got != want { 273 | t.Errorf("got [%v] want [%v]", got, want) 274 | } 275 | if got, want := lit, tc.src; got != want { 276 | t.Errorf("got [%v] want [%v]", got, want) 277 | } 278 | } 279 | } 280 | func TestFullIdentStartingWithKeyword(t *testing.T) { 281 | for _, tc := range []struct { 282 | src string 283 | }{ 284 | {"service"}, 285 | {"enum_service"}, 286 | {"message_enum.service"}, 287 | } { 288 | p := newParserOn(tc.src) 289 | _, tok, lit := p.nextFullIdent(true) 290 | if got, want := tok, tIDENT; got != want { 291 | t.Errorf("got [%v] want [%v]", got, want) 292 | } 293 | if got, want := lit, tc.src; got != want { 294 | t.Errorf("got [%v] want [%v]", got, want) 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /proto.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | // Proto represents a .proto definition 27 | type Proto struct { 28 | Filename string 29 | Elements []Visitee 30 | } 31 | 32 | // Accept dispatches the call to the visitor. 33 | func (proto *Proto) Accept(v Visitor) { 34 | // As Proto is not (yet) a Visitee, we enumerate its elements instead 35 | //v.VisitProto(proto) 36 | for _, each := range proto.Elements { 37 | each.Accept(v) 38 | } 39 | } 40 | 41 | // addElement is part of elementContainer 42 | func (proto *Proto) addElement(v Visitee) { 43 | v.parent(proto) 44 | proto.Elements = append(proto.Elements, v) 45 | } 46 | 47 | // elements is part of elementContainer 48 | func (proto *Proto) elements() []Visitee { 49 | return proto.Elements 50 | } 51 | 52 | // takeLastComment is part of elementContainer 53 | // removes and returns the last element of the list if it is a Comment. 54 | func (proto *Proto) takeLastComment(expectedOnLine int) (last *Comment) { 55 | last, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, expectedOnLine) 56 | return 57 | } 58 | 59 | // parse parsers a complete .proto definition source. 60 | func (proto *Proto) parse(p *Parser) error { 61 | for { 62 | pos, tok, lit := p.next() 63 | switch { 64 | case isComment(lit): 65 | if com := mergeOrReturnComment(proto.Elements, lit, pos); com != nil { // not merged? 66 | proto.Elements = append(proto.Elements, com) 67 | } 68 | case tOPTION == tok: 69 | o := new(Option) 70 | o.Position = pos 71 | o.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 72 | if err := o.parse(p); err != nil { 73 | return err 74 | } 75 | proto.addElement(o) 76 | case tSYNTAX == tok: 77 | s := new(Syntax) 78 | s.Position = pos 79 | s.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 80 | if err := s.parse(p); err != nil { 81 | return err 82 | } 83 | proto.addElement(s) 84 | case tEDITION == tok: 85 | s := new(Edition) 86 | s.Position = pos 87 | s.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 88 | if err := s.parse(p); err != nil { 89 | return err 90 | } 91 | proto.addElement(s) 92 | case tIMPORT == tok: 93 | im := new(Import) 94 | im.Position = pos 95 | im.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 96 | if err := im.parse(p); err != nil { 97 | return err 98 | } 99 | proto.addElement(im) 100 | case tENUM == tok: 101 | enum := new(Enum) 102 | enum.Position = pos 103 | enum.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 104 | if err := enum.parse(p); err != nil { 105 | return err 106 | } 107 | proto.addElement(enum) 108 | case tSERVICE == tok: 109 | service := new(Service) 110 | service.Position = pos 111 | service.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 112 | err := service.parse(p) 113 | if err != nil { 114 | return err 115 | } 116 | proto.addElement(service) 117 | case tPACKAGE == tok: 118 | pkg := new(Package) 119 | pkg.Position = pos 120 | pkg.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 121 | if err := pkg.parse(p); err != nil { 122 | return err 123 | } 124 | proto.addElement(pkg) 125 | case tMESSAGE == tok: 126 | msg := new(Message) 127 | msg.Position = pos 128 | msg.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 129 | if err := msg.parse(p); err != nil { 130 | return err 131 | } 132 | proto.addElement(msg) 133 | // BEGIN proto2 134 | case tEXTEND == tok: 135 | msg := new(Message) 136 | msg.Position = pos 137 | msg.Comment, proto.Elements = takeLastCommentIfEndsOnLine(proto.Elements, pos.Line-1) 138 | msg.IsExtend = true 139 | if err := msg.parse(p); err != nil { 140 | return err 141 | } 142 | proto.addElement(msg) 143 | // END proto2 144 | case tSEMICOLON == tok: 145 | maybeScanInlineComment(p, proto) 146 | // continue 147 | case tEOF == tok: 148 | goto done 149 | default: 150 | return p.unexpected(lit, ".proto element {comment|option|import|syntax|enum|service|package|message}", p) 151 | } 152 | } 153 | done: 154 | return nil 155 | } 156 | 157 | func (proto *Proto) parent(v Visitee) {} 158 | 159 | // elementContainer unifies types that have elements. 160 | type elementContainer interface { 161 | addElement(v Visitee) 162 | elements() []Visitee 163 | takeLastComment(expectedOnLine int) *Comment 164 | } 165 | -------------------------------------------------------------------------------- /protobuf_test.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func fetchAndParse(t *testing.T, url string) *Proto { 10 | resp, err := http.Get(url) 11 | if err != nil { 12 | t.Fatal(url, err) 13 | } 14 | defer resp.Body.Close() 15 | parser := NewParser(resp.Body) 16 | def, err := parser.Parse() 17 | if err != nil { 18 | t.Fatal(url, err) 19 | } 20 | t.Log("elements:", len(def.Elements)) 21 | return def 22 | } 23 | 24 | // PB=y go test -v -run ^TestPublicProtoDefinitions$ 25 | func TestPublicProtoDefinitions(t *testing.T) { 26 | if len(os.Getenv("PB")) == 0 { 27 | t.Skip("PB test not run") 28 | } 29 | for _, each := range []string{ 30 | "https://raw.githubusercontent.com/gogo/protobuf/master/test/thetest.proto", 31 | "https://raw.githubusercontent.com/gogo/protobuf/master/test/theproto3/theproto3.proto", 32 | "https://raw.githubusercontent.com/googleapis/googleapis/master/google/privacy/dlp/v2/dlp.proto", 33 | // "https://raw.githubusercontent.com/envoyproxy/data-plane-api/master/envoy/api/v2/auth/cert.proto", 34 | } { 35 | def := fetchAndParse(t, each) 36 | checkParent(def, t) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /range.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "fmt" 28 | "strconv" 29 | ) 30 | 31 | // Range is to specify number intervals (with special end value "max") 32 | type Range struct { 33 | From, To int 34 | Max bool 35 | } 36 | 37 | // SourceRepresentation return a single number if from = to. Returns to otherwise unless Max then return to max. 38 | func (r Range) SourceRepresentation() string { 39 | if r.Max { 40 | return fmt.Sprintf("%d to max", r.From) 41 | } 42 | if r.From == r.To { 43 | return strconv.Itoa(r.From) 44 | } 45 | return fmt.Sprintf("%d to %d", r.From, r.To) 46 | } 47 | 48 | // parseRanges is used to parse ranges for extensions and reserved 49 | func parseRanges(p *Parser, n Visitee) (list []Range, err error) { 50 | seenTo := false 51 | negate := false // for numbers 52 | for { 53 | pos, tok, lit := p.next() 54 | if isString(lit) { 55 | return list, p.unexpected(lit, "integer, ", n) 56 | } 57 | switch lit { 58 | case "-": 59 | negate = true 60 | case ",": 61 | case "to": 62 | seenTo = true 63 | case ";": 64 | p.nextPut(pos, tok, lit) // allow for inline comment parsing 65 | goto done 66 | case "max": 67 | if !seenTo { 68 | return list, p.unexpected(lit, "to", n) 69 | } 70 | from := list[len(list)-1] 71 | list = append(list[0:len(list)-1], Range{From: from.From, Max: true}) 72 | default: 73 | // must be number 74 | i, err := strconv.Atoi(lit) 75 | if err != nil { 76 | return list, p.unexpected(lit, "range integer", n) 77 | } 78 | if negate { 79 | i = -i 80 | negate = false 81 | } 82 | if seenTo { 83 | // replace last two ranges with one 84 | if len(list) < 1 { 85 | p.unexpected(lit, "integer", n) 86 | } 87 | from := list[len(list)-1] 88 | list = append(list[0:len(list)-1], Range{From: from.From, To: i}) 89 | seenTo = false 90 | } else { 91 | list = append(list, Range{From: i, To: i}) 92 | } 93 | } 94 | } 95 | done: 96 | return 97 | } 98 | -------------------------------------------------------------------------------- /range_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestParseRanges(t *testing.T) { 29 | r := new(Reserved) 30 | p := newParserOn(`reserved 2, 15, 9 to 11;`) 31 | _, _, _ = p.next() 32 | ranges, err := parseRanges(p, r) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | if got, want := ranges[2].SourceRepresentation(), "9 to 11"; got != want { 37 | t.Errorf("got [%v] want [%v]", got, want) 38 | } 39 | } 40 | 41 | func TestParseRangesMax(t *testing.T) { 42 | r := new(Extensions) 43 | p := newParserOn(`extensions 3 to max;`) 44 | _, _, _ = p.next() 45 | ranges, err := parseRanges(p, r) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if got, want := ranges[0].SourceRepresentation(), "3 to max"; got != want { 50 | t.Errorf("got [%v] want [%v]", got, want) 51 | } 52 | } 53 | 54 | func TestParseRangesMultiToMax(t *testing.T) { 55 | r := new(Extensions) 56 | p := newParserOn(`extensions 1,2 to 5,6 to 9,10 to max;`) 57 | _, _, _ = p.next() 58 | ranges, err := parseRanges(p, r) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | if got, want := len(ranges), 4; got != want { 63 | t.Fatalf("got [%v] want [%v]", got, want) 64 | } 65 | if got, want := ranges[3].SourceRepresentation(), "10 to max"; got != want { 66 | t.Errorf("got [%v] want [%v]", got, want) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /reserved.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "text/scanner" 27 | 28 | // Reserved statements declare a range of field numbers or field names that cannot be used in a message. 29 | type Reserved struct { 30 | Position scanner.Position 31 | Comment *Comment 32 | Ranges []Range 33 | FieldNames []string 34 | InlineComment *Comment 35 | Parent Visitee 36 | } 37 | 38 | // inlineComment is part of commentInliner. 39 | func (r *Reserved) inlineComment(c *Comment) { 40 | r.InlineComment = c 41 | } 42 | 43 | // Accept dispatches the call to the visitor. 44 | func (r *Reserved) Accept(v Visitor) { 45 | v.VisitReserved(r) 46 | } 47 | 48 | func (r *Reserved) parse(p *Parser) error { 49 | for { 50 | pos, tok, lit := p.next() 51 | if len(lit) == 0 { 52 | return p.unexpected(lit, "reserved string or integer", r) 53 | } 54 | // first char that determined tok 55 | ch := []rune(lit)[0] 56 | if isDigit(ch) || ch == '-' { 57 | // use unread here because it could be start of ranges 58 | p.nextPut(pos, tok, lit) 59 | list, err := parseRanges(p, r) 60 | if err != nil { 61 | return err 62 | } 63 | r.Ranges = list 64 | continue 65 | } 66 | if isString(lit) { 67 | s, _ := unQuote(lit) 68 | r.FieldNames = append(r.FieldNames, s) 69 | continue 70 | } 71 | if tSEMICOLON == tok { 72 | p.nextPut(pos, tok, lit) 73 | break 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | func (r *Reserved) parent(v Visitee) { r.Parent = v } 80 | -------------------------------------------------------------------------------- /reserved_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestReservedRanges(t *testing.T) { 29 | r := new(Reserved) 30 | p := newParserOn(`reserved 2, 15, 9 to 11;`) 31 | _, tok, _ := p.next() 32 | if tRESERVED != tok { 33 | t.Fail() 34 | } 35 | err := r.parse(p) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | if got, want := r.Ranges[0].SourceRepresentation(), "2"; got != want { 40 | t.Fatalf("got [%v] want [%v]", got, want) 41 | } 42 | if got, want := r.Ranges[2].SourceRepresentation(), "9 to 11"; got != want { 43 | t.Errorf("got [%v] want [%v]", got, want) 44 | } 45 | } 46 | 47 | func TestReservedFieldNames(t *testing.T) { 48 | r := new(Reserved) 49 | p := newParserOn(`reserved "foo", "bar";`) 50 | _, _, _ = p.next() 51 | err := r.parse(p) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | if got, want := len(r.FieldNames), 2; got != want { 56 | t.Fatalf("got [%v] want [%v]", got, want) 57 | } 58 | if got, want := r.FieldNames[0], "foo"; got != want { 59 | t.Errorf("got [%v] want [%v]", got, want) 60 | } 61 | if got, want := r.FieldNames[1], "bar"; got != want { 62 | t.Errorf("got [%v] want [%v]", got, want) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Service defines a set of RPC calls. 31 | type Service struct { 32 | Position scanner.Position 33 | Comment *Comment 34 | Name string 35 | Elements []Visitee 36 | Parent Visitee 37 | } 38 | 39 | // Accept dispatches the call to the visitor. 40 | func (s *Service) Accept(v Visitor) { 41 | v.VisitService(s) 42 | } 43 | 44 | // Doc is part of Documented 45 | func (s *Service) Doc() *Comment { 46 | return s.Comment 47 | } 48 | 49 | // addElement is part of elementContainer 50 | func (s *Service) addElement(v Visitee) { 51 | v.parent(s) 52 | s.Elements = append(s.Elements, v) 53 | } 54 | 55 | // elements is part of elementContainer 56 | func (s *Service) elements() []Visitee { 57 | return s.Elements 58 | } 59 | 60 | // takeLastComment is part of elementContainer 61 | // removes and returns the last elements of the list if it is a Comment. 62 | func (s *Service) takeLastComment(expectedOnLine int) (last *Comment) { 63 | last, s.Elements = takeLastCommentIfEndsOnLine(s.Elements, expectedOnLine) 64 | return 65 | } 66 | 67 | // parse continues after reading "service" 68 | func (s *Service) parse(p *Parser) error { 69 | pos, tok, lit := p.nextIdentifier() 70 | if tok != tIDENT { 71 | if !isKeyword(tok) { 72 | return p.unexpected(lit, "service identifier", s) 73 | } 74 | } 75 | s.Name = lit 76 | consumeCommentFor(p, s) 77 | pos, tok, lit = p.next() 78 | if tok != tLEFTCURLY { 79 | return p.unexpected(lit, "service opening {", s) 80 | } 81 | for { 82 | pos, tok, lit = p.next() 83 | switch tok { 84 | case tCOMMENT: 85 | if com := mergeOrReturnComment(s.Elements, lit, pos); com != nil { // not merged? 86 | s.addElement(com) 87 | } 88 | case tOPTION: 89 | opt := new(Option) 90 | opt.Position = pos 91 | opt.Comment, s.Elements = takeLastCommentIfEndsOnLine(s.elements(), pos.Line-1) 92 | if err := opt.parse(p); err != nil { 93 | return err 94 | } 95 | s.addElement(opt) 96 | case tRPC: 97 | rpc := new(RPC) 98 | rpc.Position = pos 99 | rpc.Comment, s.Elements = takeLastCommentIfEndsOnLine(s.Elements, pos.Line-1) 100 | err := rpc.parse(p) 101 | if err != nil { 102 | return err 103 | } 104 | s.addElement(rpc) 105 | maybeScanInlineComment(p, s) 106 | case tSEMICOLON: 107 | maybeScanInlineComment(p, s) 108 | case tRIGHTCURLY: 109 | goto done 110 | default: 111 | return p.unexpected(lit, "service comment|rpc", s) 112 | } 113 | } 114 | done: 115 | return nil 116 | } 117 | 118 | func (s *Service) parent(v Visitee) { s.Parent = v } 119 | 120 | // RPC represents an rpc entry in a message. 121 | type RPC struct { 122 | Position scanner.Position 123 | Comment *Comment 124 | Name string 125 | RequestType string 126 | StreamsRequest bool 127 | ReturnsType string 128 | StreamsReturns bool 129 | Elements []Visitee 130 | InlineComment *Comment 131 | Parent Visitee 132 | 133 | // Options field is DEPRECATED, use Elements instead. 134 | Options []*Option 135 | } 136 | 137 | // Accept dispatches the call to the visitor. 138 | func (r *RPC) Accept(v Visitor) { 139 | v.VisitRPC(r) 140 | } 141 | 142 | // Doc is part of Documented 143 | func (r *RPC) Doc() *Comment { 144 | return r.Comment 145 | } 146 | 147 | // inlineComment is part of commentInliner. 148 | func (r *RPC) inlineComment(c *Comment) { 149 | r.InlineComment = c 150 | } 151 | 152 | // parse continues after reading "rpc" 153 | func (r *RPC) parse(p *Parser) error { 154 | pos, tok, lit := p.next() 155 | if tok != tIDENT { 156 | return p.unexpected(lit, "rpc method", r) 157 | } 158 | r.Name = lit 159 | pos, tok, lit = p.next() 160 | if tok != tLEFTPAREN { 161 | return p.unexpected(lit, "rpc type opening (", r) 162 | } 163 | pos, tok, lit = p.nextTypeName() 164 | if tSTREAM == tok { 165 | r.StreamsRequest = true 166 | pos, tok, lit = p.nextTypeName() 167 | } 168 | if tok != tIDENT { 169 | return p.unexpected(lit, "rpc stream | request type", r) 170 | } 171 | r.RequestType = lit 172 | pos, tok, lit = p.next() 173 | if tok != tRIGHTPAREN { 174 | return p.unexpected(lit, "rpc type closing )", r) 175 | } 176 | pos, tok, lit = p.next() 177 | if tok != tRETURNS { 178 | return p.unexpected(lit, "rpc returns", r) 179 | } 180 | pos, tok, lit = p.next() 181 | if tok != tLEFTPAREN { 182 | return p.unexpected(lit, "rpc type opening (", r) 183 | } 184 | pos, tok, lit = p.nextTypeName() 185 | if tSTREAM == tok { 186 | r.StreamsReturns = true 187 | pos, tok, lit = p.nextTypeName() 188 | } 189 | if tok != tIDENT { 190 | return p.unexpected(lit, "rpc stream | returns type", r) 191 | } 192 | r.ReturnsType = lit 193 | pos, tok, lit = p.next() 194 | if tok != tRIGHTPAREN { 195 | return p.unexpected(lit, "rpc type closing )", r) 196 | } 197 | pos, tok, lit = p.next() 198 | if tSEMICOLON == tok { 199 | p.nextPut(pos, tok, lit) // allow for inline comment parsing 200 | return nil 201 | } 202 | if tLEFTCURLY == tok { 203 | // parse options 204 | for { 205 | pos, tok, lit = p.next() 206 | if tRIGHTCURLY == tok { 207 | break 208 | } 209 | if isComment(lit) { 210 | if com := mergeOrReturnComment(r.elements(), lit, pos); com != nil { // not merged? 211 | r.addElement(com) 212 | continue 213 | } 214 | } 215 | if tSEMICOLON == tok { 216 | maybeScanInlineComment(p, r) 217 | continue 218 | } 219 | if tOPTION == tok { 220 | o := new(Option) 221 | o.Position = pos 222 | if err := o.parse(p); err != nil { 223 | return err 224 | } 225 | r.addElement(o) 226 | } 227 | } 228 | } 229 | return nil 230 | } 231 | 232 | // addElement is part of elementContainer 233 | func (r *RPC) addElement(v Visitee) { 234 | v.parent(r) 235 | r.Elements = append(r.Elements, v) 236 | // handle deprecated field 237 | if option, ok := v.(*Option); ok { 238 | r.Options = append(r.Options, option) 239 | } 240 | } 241 | 242 | // elements is part of elementContainer 243 | func (r *RPC) elements() []Visitee { 244 | return r.Elements 245 | } 246 | 247 | func (r *RPC) takeLastComment(expectedOnLine int) (last *Comment) { 248 | last, r.Elements = takeLastCommentIfEndsOnLine(r.Elements, expectedOnLine) 249 | return 250 | } 251 | 252 | func (r *RPC) parent(v Visitee) { r.Parent = v } 253 | -------------------------------------------------------------------------------- /service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestService(t *testing.T) { 29 | proto := `service AccountService { 30 | // comment 31 | rpc CreateAccount (CreateAccount) returns (ServiceFault); // inline comment 32 | rpc GetAccounts (stream Int64) returns (Account) {} // inline comment2 33 | rpc Health(google.protobuf.Empty) returns (google.protobuf.Empty) {} // inline comment3 34 | }` 35 | pr, err := newParserOn(proto).Parse() 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | srv := collect(pr).Services()[0] 40 | if got, want := len(srv.Elements), 3; got != want { 41 | t.Fatalf("got [%v] want [%v]", got, want) 42 | } 43 | if got, want := srv.Position.String(), ":1:1"; got != want { 44 | t.Fatalf("got [%v] want [%v]", got, want) 45 | } 46 | rpc1 := srv.Elements[0].(*RPC) 47 | if got, want := rpc1.Name, "CreateAccount"; got != want { 48 | t.Fatalf("got [%v] want [%v]", got, want) 49 | } 50 | if got, want := rpc1.Doc().Message(), " comment"; got != want { 51 | t.Fatalf("got [%v] want [%v]", got, want) 52 | } 53 | if got, want := rpc1.InlineComment.Message(), " inline comment"; got != want { 54 | t.Fatalf("got [%v] want [%v]", got, want) 55 | } 56 | if got, want := rpc1.Position.Line, 3; got != want { 57 | t.Fatalf("got [%v] want [%v]", got, want) 58 | } 59 | rpc2 := srv.Elements[1].(*RPC) 60 | if got, want := rpc2.Name, "GetAccounts"; got != want { 61 | t.Errorf("got [%v] want [%v]", got, want) 62 | } 63 | rpc3 := srv.Elements[2].(*RPC) 64 | if got, want := rpc3.Name, "Health"; got != want { 65 | t.Errorf("got [%v] want [%v]", got, want) 66 | } 67 | if rpc2.InlineComment == nil { 68 | t.Fatal("missing inline comment 2") 69 | } 70 | if got, want := rpc2.InlineComment.Message(), " inline comment2"; got != want { 71 | t.Fatalf("got [%v] want [%v]", got, want) 72 | } 73 | if rpc3.InlineComment == nil { 74 | t.Fatal("missing inline comment 3") 75 | } 76 | if got, want := rpc3.InlineComment.Message(), " inline comment3"; got != want { 77 | t.Fatalf("got [%v] want [%v]", got, want) 78 | } 79 | } 80 | 81 | func TestRPCWithOptionAggregateSyntax(t *testing.T) { 82 | proto := `service AccountService { 83 | // CreateAccount 84 | rpc CreateAccount (CreateAccount) returns (ServiceFault){ 85 | // test_ident 86 | option (test_ident) = { 87 | test: "test" 88 | test2:"test2" 89 | }; // inline test_ident 90 | } 91 | }` 92 | pr, err := newParserOn(proto).Parse() 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | srv := collect(pr).Services()[0] 97 | if got, want := len(srv.Elements), 1; got != want { 98 | t.Fatalf("got [%v] want [%v]", got, want) 99 | } 100 | rpc1 := srv.Elements[0].(*RPC) 101 | if got, want := len(rpc1.Elements), 2; got != want { 102 | t.Errorf("got [%v] want [%v]", got, want) 103 | } 104 | com := rpc1.Elements[0].(*Comment) 105 | if got, want := com.Message(), " test_ident"; got != want { 106 | t.Errorf("got [%v] want [%v]", got, want) 107 | } 108 | opt := rpc1.Elements[1].(*Option) 109 | if got, want := opt.Name, "(test_ident)"; got != want { 110 | t.Errorf("got [%v] want [%v]", got, want) 111 | } 112 | if got, want := opt.InlineComment != nil, true; got != want { 113 | t.Fatalf("got [%v] want [%v]", got, want) 114 | } 115 | if got, want := opt.InlineComment.Message(), " inline test_ident"; got != want { 116 | t.Errorf("got [%v] want [%v]", got, want) 117 | } 118 | if got, want := len(opt.Constant.Map), 2; got != want { 119 | t.Fatalf("got [%v] want [%v]", got, want) 120 | } 121 | // test deprecated field Options in RPC 122 | if got, want := len(rpc1.Options), 1; got != want { 123 | t.Errorf("got len Options %v want %v", got, want) 124 | } 125 | } 126 | 127 | func TestServiceWithOption(t *testing.T) { 128 | src := `service AnyService { 129 | option secure = true; 130 | }` 131 | p := newParserOn(src) 132 | p.next() 133 | svc := new(Service) 134 | err := svc.parse(p) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | if got, want := svc.Elements[0].(*Option).Name, "secure"; got != want { 139 | t.Errorf("got [%v] want [%v]", got, want) 140 | } 141 | checkParent(svc.Elements[0].(*Option), t) 142 | } 143 | 144 | func TestRPCWithOneLineCommentInOptionBlock(t *testing.T) { 145 | proto := `service AccountService { 146 | rpc CreateAccount (CreateAccount) returns (ServiceFault) { 147 | // test comment 148 | } 149 | }` 150 | _, err := newParserOn(proto).Parse() 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | } 155 | 156 | func TestRPCWithMultiLineCommentInOptionBlock(t *testing.T) { 157 | proto := `service AccountService { 158 | rpc CreateAccount (CreateAccount) returns (ServiceFault) { 159 | // test comment 160 | // test comment 161 | } 162 | }` 163 | def, err := newParserOn(proto).Parse() 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | s := def.Elements[0].(*Service) 168 | r := s.Elements[0].(*RPC) 169 | if got, want := len(r.Elements), 1; got != want { 170 | t.Errorf("got [%v] want [%v]", got, want) 171 | } 172 | c := r.Elements[0].(*Comment) 173 | if got, want := len(c.Lines), 2; got != want { 174 | t.Errorf("got [%v] want [%v]", got, want) 175 | } 176 | } 177 | 178 | func TestRPCWithTypeThatHasLeadingDot(t *testing.T) { 179 | src := `service Dummy { 180 | rpc DeleteProgram (ProgramIdentifier) returns (.google.protobuf.Empty) {} 181 | }` 182 | _, err := newParserOn(src).Parse() 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | } 187 | 188 | func TestServiceInlineCommentBeforeBody(t *testing.T) { 189 | src := `service BarService // BarService 190 | // with another line 191 | { 192 | rpc Foo (Void) returns (Magic) // FooRPC { 193 | // yet another line 194 | } 195 | } 196 | ` 197 | p := newParserOn(src) 198 | svc := new(Service) 199 | p.next() 200 | if err := svc.parse(p); err != nil { 201 | t.Fatal(err) 202 | } 203 | nestedComment := svc.Elements[0].(*Comment) 204 | if nestedComment == nil { 205 | t.Fatal("expected comment present") 206 | } 207 | if got, want := len(nestedComment.Lines), 2; got != want { 208 | t.Errorf("got %d want %d lines", got, want) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /syntax.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "text/scanner" 28 | ) 29 | 30 | // Syntax should have value "proto" 31 | type Syntax struct { 32 | Position scanner.Position 33 | Comment *Comment 34 | Value string 35 | InlineComment *Comment 36 | Parent Visitee 37 | } 38 | 39 | func (s *Syntax) parse(p *Parser) error { 40 | if _, tok, lit := p.next(); tok != tEQUALS { 41 | return p.unexpected(lit, "syntax =", s) 42 | } 43 | _, _, lit := p.next() 44 | if !isString(lit) { 45 | return p.unexpected(lit, "syntax string constant", s) 46 | } 47 | s.Value, _ = unQuote(lit) 48 | return nil 49 | } 50 | 51 | // Accept dispatches the call to the visitor. 52 | func (s *Syntax) Accept(v Visitor) { 53 | v.VisitSyntax(s) 54 | } 55 | 56 | // Doc is part of Documented 57 | func (s *Syntax) Doc() *Comment { 58 | return s.Comment 59 | } 60 | 61 | // inlineComment is part of commentInliner. 62 | func (s *Syntax) inlineComment(c *Comment) { 63 | s.InlineComment = c 64 | } 65 | 66 | func (s *Syntax) parent(v Visitee) { s.Parent = v } 67 | -------------------------------------------------------------------------------- /syntax_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestSyntax(t *testing.T) { 29 | proto := `syntax = "proto";` 30 | p := newParserOn(proto) 31 | p.next() // consume first token 32 | s := new(Syntax) 33 | err := s.parse(p) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if got, want := s.Value, "proto"; got != want { 38 | t.Errorf("got [%v] want [%v]", got, want) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import ( 27 | "strconv" 28 | "strings" 29 | ) 30 | 31 | // token represents a lexical token. 32 | type token int 33 | 34 | const ( 35 | // Special tokens 36 | tILLEGAL token = iota 37 | tEOF 38 | tWS 39 | 40 | // Literals 41 | tIDENT 42 | 43 | // Misc characters 44 | tSEMICOLON // ; 45 | tCOLON // : 46 | tEQUALS // = 47 | tQUOTE // " 48 | tSINGLEQUOTE // ' 49 | tLEFTPAREN // ( 50 | tRIGHTPAREN // ) 51 | tLEFTCURLY // { 52 | tRIGHTCURLY // } 53 | tLEFTSQUARE // [ 54 | tRIGHTSQUARE // ] 55 | tCOMMENT // / 56 | tLESS // < 57 | tGREATER // > 58 | tCOMMA // , 59 | tDOT // . 60 | 61 | // Keywords 62 | keywordsStart 63 | tEDITION 64 | tSYNTAX 65 | tSERVICE 66 | tRPC 67 | tRETURNS 68 | tMESSAGE 69 | tIMPORT 70 | tPACKAGE 71 | tOPTION 72 | tREPEATED 73 | tWEAK 74 | tPUBLIC 75 | 76 | // special fields 77 | tONEOF 78 | tMAP 79 | tRESERVED 80 | tENUM 81 | tSTREAM 82 | 83 | // numbers (pos or neg, float) 84 | tNUMBER 85 | 86 | // BEGIN proto2 87 | tOPTIONAL 88 | tGROUP 89 | tEXTENSIONS 90 | tEXTEND 91 | tREQUIRED 92 | // END proto2 93 | keywordsEnd 94 | ) 95 | 96 | // typeTokens exists for future validation 97 | const typeTokens = "double float int32 int64 uint32 uint64 sint32 sint64 fixed32 sfixed32 sfixed64 bool string bytes" 98 | 99 | // isKeyword returns if tok is in the keywords range 100 | func isKeyword(tok token) bool { 101 | return keywordsStart < tok && tok < keywordsEnd 102 | } 103 | 104 | // isWhitespace checks for space,tab and newline 105 | func isWhitespace(r rune) bool { 106 | return r == ' ' || r == '\t' || r == '\n' 107 | } 108 | 109 | // isDigit returns true if the rune is a digit. 110 | func isDigit(ch rune) bool { return (ch >= '0' && ch <= '9') } 111 | 112 | // isString checks if the literal is quoted (single or double). 113 | func isString(lit string) bool { 114 | if lit == "'" { 115 | return false 116 | } 117 | return (strings.HasPrefix(lit, "\"") && 118 | strings.HasSuffix(lit, "\"")) || 119 | (strings.HasPrefix(lit, "'") && 120 | strings.HasSuffix(lit, "'")) 121 | } 122 | 123 | func isComment(lit string) bool { 124 | return strings.HasPrefix(lit, "//") || strings.HasPrefix(lit, "/*") 125 | } 126 | 127 | func isNumber(lit string) bool { 128 | if lit == "NaN" || lit == "nan" || lit == "Inf" || lit == "Infinity" || lit == "inf" || lit == "infinity" { 129 | return false 130 | } 131 | if strings.HasPrefix(lit, "0x") || strings.HasPrefix(lit, "0X") { 132 | _, err := strconv.ParseInt(lit, 0, 64) 133 | return err == nil 134 | } 135 | _, err := strconv.ParseFloat(lit, 64) 136 | return err == nil 137 | } 138 | 139 | const doubleQuoteRune = rune('"') 140 | 141 | // unQuote removes one matching leading and trailing single or double quote. 142 | // 143 | // https://github.com/emicklei/proto/issues/103 144 | // cannot use strconv.Unquote as this unescapes quotes. 145 | func unQuote(lit string) (string, rune) { 146 | if len(lit) < 2 { 147 | return lit, doubleQuoteRune 148 | } 149 | chars := []rune(lit) 150 | first, last := chars[0], chars[len(chars)-1] 151 | if first != last { 152 | return lit, doubleQuoteRune 153 | } 154 | if s := string(chars[0]); s == "\"" || s == stringWithSingleQuote { 155 | return string(chars[1 : len(chars)-1]), chars[0] 156 | } 157 | return lit, doubleQuoteRune 158 | } 159 | 160 | func asToken(literal string) token { 161 | switch literal { 162 | // delimiters 163 | case ";": 164 | return tSEMICOLON 165 | case ":": 166 | return tCOLON 167 | case "=": 168 | return tEQUALS 169 | case "\"": 170 | return tQUOTE 171 | case "'": 172 | return tSINGLEQUOTE 173 | case "(": 174 | return tLEFTPAREN 175 | case ")": 176 | return tRIGHTPAREN 177 | case "{": 178 | return tLEFTCURLY 179 | case "}": 180 | return tRIGHTCURLY 181 | case "[": 182 | return tLEFTSQUARE 183 | case "]": 184 | return tRIGHTSQUARE 185 | case "<": 186 | return tLESS 187 | case ">": 188 | return tGREATER 189 | case ",": 190 | return tCOMMA 191 | case ".": 192 | return tDOT 193 | // words 194 | case "syntax": 195 | return tSYNTAX 196 | case "edition": 197 | return tEDITION 198 | case "service": 199 | return tSERVICE 200 | case "rpc": 201 | return tRPC 202 | case "returns": 203 | return tRETURNS 204 | case "option": 205 | return tOPTION 206 | case "message": 207 | return tMESSAGE 208 | case "import": 209 | return tIMPORT 210 | case "package": 211 | return tPACKAGE 212 | case "oneof": 213 | return tONEOF 214 | // special fields 215 | case "map": 216 | return tMAP 217 | case "reserved": 218 | return tRESERVED 219 | case "enum": 220 | return tENUM 221 | case "repeated": 222 | return tREPEATED 223 | case "weak": 224 | return tWEAK 225 | case "public": 226 | return tPUBLIC 227 | case "stream": 228 | return tSTREAM 229 | // proto2 230 | case "optional": 231 | return tOPTIONAL 232 | case "group": 233 | return tGROUP 234 | case "extensions": 235 | return tEXTENSIONS 236 | case "extend": 237 | return tEXTEND 238 | case "required": 239 | return tREQUIRED 240 | default: 241 | // special cases 242 | if isNumber(literal) { 243 | return tNUMBER 244 | } 245 | if isComment(literal) { 246 | return tCOMMENT 247 | } 248 | return tIDENT 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | import "testing" 27 | 28 | func TestUnQuoteCases(t *testing.T) { 29 | singleQuoteRune := rune('\'') 30 | for i, each := range []struct { 31 | input, output string 32 | quoteRune rune 33 | }{ 34 | {"thanos", "thanos", doubleQuoteRune}, 35 | {"`bucky`", "`bucky`", doubleQuoteRune}, 36 | {"'nat", "'nat", doubleQuoteRune}, 37 | {"'bruce'", "bruce", singleQuoteRune}, 38 | {"\"tony\"", "tony", doubleQuoteRune}, 39 | {"\"'\"\"' -> \"\"\"\"\"\"", `'""' -> """""`, doubleQuoteRune}, 40 | {`"''"`, "''", doubleQuoteRune}, 41 | {"''", "", singleQuoteRune}, 42 | {"", "", doubleQuoteRune}, 43 | } { 44 | got, gotRune := unQuote(each.input) 45 | if gotRune != each.quoteRune { 46 | t.Errorf("[%d] got [%v] want [%v]", i, gotRune, each.quoteRune) 47 | } 48 | want := each.output 49 | if got != want { 50 | t.Errorf("[%d] got [%s] want [%s]", i, got, want) 51 | } 52 | } 53 | } 54 | 55 | func TestIsNumber(t *testing.T) { 56 | for i, each := range []struct { 57 | input string 58 | isNumber bool 59 | }{ 60 | {`1`, true}, 61 | {`1.2`, true}, 62 | {`-1.02`, true}, 63 | {`a1`, false}, 64 | {`0x12`, true}, 65 | {`0X77777`, true}, 66 | {`NaN`, false}, 67 | {`nan`, false}, 68 | {`Inf`, false}, 69 | {`Infinity`, false}, 70 | {`inf`, false}, 71 | {`infinity`, false}, 72 | } { 73 | got := isNumber(each.input) 74 | if got != each.isNumber { 75 | t.Errorf("[%d] got [%v] want [%v]", i, got, each.isNumber) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /visitor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | // Visitor is for dispatching Proto elements. 27 | type Visitor interface { 28 | VisitMessage(m *Message) 29 | VisitService(v *Service) 30 | VisitSyntax(s *Syntax) 31 | VisitPackage(p *Package) 32 | VisitOption(o *Option) 33 | VisitImport(i *Import) 34 | VisitNormalField(i *NormalField) 35 | VisitEnumField(i *EnumField) 36 | VisitEnum(e *Enum) 37 | VisitComment(e *Comment) 38 | VisitOneof(o *Oneof) 39 | VisitOneofField(o *OneOfField) 40 | VisitReserved(r *Reserved) 41 | VisitRPC(r *RPC) 42 | VisitMapField(f *MapField) 43 | // proto2 44 | VisitGroup(g *Group) 45 | VisitExtensions(e *Extensions) 46 | // edition (proto3+), v2 47 | // VisitEdition(e *Edition) 48 | } 49 | 50 | // Visitee is implemented by all Proto elements. 51 | type Visitee interface { 52 | Accept(v Visitor) 53 | parent(e Visitee) 54 | } 55 | 56 | // Documented is for types that may have an associated comment (not inlined). 57 | type Documented interface { 58 | Doc() *Comment 59 | } 60 | -------------------------------------------------------------------------------- /visitor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | type collector struct { 27 | proto *Proto 28 | } 29 | 30 | func collect(p *Proto) collector { 31 | return collector{p} 32 | } 33 | 34 | func (c collector) Comments() (list []*Comment) { 35 | for _, each := range c.proto.Elements { 36 | if c, ok := each.(*Comment); ok { 37 | list = append(list, c) 38 | } 39 | } 40 | return 41 | } 42 | 43 | func (c collector) Enums() (list []*Enum) { 44 | for _, each := range c.proto.Elements { 45 | if c, ok := each.(*Enum); ok { 46 | list = append(list, c) 47 | } 48 | } 49 | return 50 | } 51 | 52 | func (c collector) Messages() (list []*Message) { 53 | for _, each := range c.proto.Elements { 54 | if c, ok := each.(*Message); ok { 55 | list = append(list, c) 56 | } 57 | } 58 | return 59 | } 60 | 61 | func (c collector) Services() (list []*Service) { 62 | for _, each := range c.proto.Elements { 63 | if c, ok := each.(*Service); ok { 64 | list = append(list, c) 65 | } 66 | } 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /walk.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Ernest Micklei 2 | // 3 | // MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | package proto 25 | 26 | // Handler is a type of function that accepts a Visitee. 27 | type Handler func(v Visitee) 28 | 29 | // Walk recursively pays a visit to all Visitees of a Proto and calls each handler with it. 30 | func Walk(proto *Proto, handlers ...Handler) { 31 | walk(proto, handlers...) 32 | } 33 | 34 | func walk(container elementContainer, handlers ...Handler) { 35 | for _, eachElement := range container.elements() { 36 | for _, eachFilter := range handlers { 37 | eachFilter(eachElement) 38 | } 39 | if next, ok := eachElement.(elementContainer); ok { 40 | walk(next, handlers...) 41 | } 42 | } 43 | } 44 | 45 | // WithImport returns a Handler that will call the apply function when the Visitee is an Import. 46 | func WithImport(apply func(*Import)) Handler { 47 | return func(v Visitee) { 48 | if s, ok := v.(*Import); ok { 49 | apply(s) 50 | } 51 | } 52 | } 53 | 54 | // WithMessage returns a Handler that will call the apply function when the Visitee is a Message. 55 | func WithMessage(apply func(*Message)) Handler { 56 | return func(v Visitee) { 57 | if s, ok := v.(*Message); ok { 58 | apply(s) 59 | } 60 | } 61 | } 62 | 63 | // WithOption returns a Handler that will call the apply function when the Visitee is a Option. 64 | func WithOption(apply func(*Option)) Handler { 65 | return func(v Visitee) { 66 | if s, ok := v.(*Option); ok { 67 | apply(s) 68 | } 69 | } 70 | } 71 | 72 | // WithEnum returns a Handler that will call the apply function when the Visitee is a Enum. 73 | func WithEnum(apply func(*Enum)) Handler { 74 | return func(v Visitee) { 75 | if s, ok := v.(*Enum); ok { 76 | apply(s) 77 | } 78 | } 79 | } 80 | 81 | // WithOneof returns a Handler that will call the apply function when the Visitee is a Oneof. 82 | func WithOneof(apply func(*Oneof)) Handler { 83 | return func(v Visitee) { 84 | if s, ok := v.(*Oneof); ok { 85 | apply(s) 86 | } 87 | } 88 | } 89 | 90 | // WithService returns a Handler that will call the apply function when the Visitee is a Service. 91 | func WithService(apply func(*Service)) Handler { 92 | return func(v Visitee) { 93 | if s, ok := v.(*Service); ok { 94 | apply(s) 95 | } 96 | } 97 | } 98 | 99 | // WithRPC returns a Handler that will call the apply function when the Visitee is a RPC. 100 | func WithRPC(apply func(*RPC)) Handler { 101 | return func(v Visitee) { 102 | if s, ok := v.(*RPC); ok { 103 | apply(s) 104 | } 105 | } 106 | } 107 | 108 | // WithPackage returns a Handler that will call the apply function when the Visitee is a Package. 109 | func WithPackage(apply func(*Package)) Handler { 110 | return func(v Visitee) { 111 | if s, ok := v.(*Package); ok { 112 | apply(s) 113 | } 114 | } 115 | } 116 | 117 | // WithNormalField returns a Handler that will call the apply function when the Visitee is a NormalField. 118 | func WithNormalField(apply func(*NormalField)) Handler { 119 | return func(v Visitee) { 120 | if s, ok := v.(*NormalField); ok { 121 | apply(s) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /walk_test.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | type counter struct { 9 | counts map[string]int 10 | } 11 | 12 | func (c counter) handleService(s *Service) { 13 | c.counts["service"] = c.counts["service"] + 1 14 | } 15 | 16 | func (c counter) handleRPC(r *RPC) { 17 | c.counts["rpc"] = c.counts["rpc"] + 1 18 | } 19 | 20 | func (c counter) handleImport(r *Import) { 21 | c.counts["import"] = c.counts["import"] + 1 22 | } 23 | 24 | func (c counter) handleNormalField(r *NormalField) { 25 | c.counts["normal field"] = c.counts["import"] + 1 26 | } 27 | 28 | func TestWalkGoogleApisDLP(t *testing.T) { 29 | if len(os.Getenv("PB")) == 0 { 30 | t.Skip("PB test not run") 31 | } 32 | proto := fetchAndParse(t, "https://raw.githubusercontent.com/gogo/protobuf/master/test/theproto3/theproto3.proto") 33 | count := counter{counts: map[string]int{}} 34 | Walk(proto, 35 | WithPackage(func(p *Package) { 36 | t.Log("package:", p.Name) 37 | }), 38 | WithService(count.handleService), 39 | WithRPC(count.handleRPC), 40 | WithImport(count.handleImport), 41 | WithNormalField(count.handleNormalField), 42 | ) 43 | t.Logf("%#v", count) 44 | } 45 | --------------------------------------------------------------------------------