github.com/jwilson-ts/prototool@v1.3.0/internal/format/base_visitor.go (about)

     1  // Copyright (c) 2018 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package format
    22  
    23  import (
    24  	"fmt"
    25  	"sort"
    26  	"strings"
    27  	"text/scanner"
    28  
    29  	"github.com/emicklei/proto"
    30  	"github.com/uber/prototool/internal/text"
    31  )
    32  
    33  type baseVisitor struct {
    34  	*printer
    35  
    36  	Failures []*text.Failure
    37  }
    38  
    39  func newBaseVisitor() *baseVisitor {
    40  	return &baseVisitor{printer: newPrinter()}
    41  }
    42  
    43  func (v *baseVisitor) AddFailure(position scanner.Position, format string, args ...interface{}) {
    44  	v.Failures = append(v.Failures, &text.Failure{
    45  		Line:    position.Line,
    46  		Column:  position.Column,
    47  		Message: fmt.Sprintf(format, args...),
    48  	})
    49  }
    50  
    51  func (v *baseVisitor) PWithInlineComment(inlineComment *proto.Comment, args ...interface{}) {
    52  	if inlineComment == nil || len(inlineComment.Lines) == 0 {
    53  		v.P(args...)
    54  		return
    55  	}
    56  	// https://github.com/emicklei/proto/commit/5a91db7561a4dedab311f36304fcf0512343a9b1
    57  	args = append(args, ` //`, cleanCommentLine(inlineComment.Lines[0]))
    58  	v.P(args...)
    59  	for _, line := range inlineComment.Lines[1:] {
    60  		v.P(`//`, cleanCommentLine(line))
    61  	}
    62  }
    63  
    64  func (v *baseVisitor) PComment(comment *proto.Comment) {
    65  	if comment == nil || len(comment.Lines) == 0 {
    66  		return
    67  	}
    68  	// https://github.com/emicklei/proto/commit/5a91db7561a4dedab311f36304fcf0512343a9b1
    69  	// this is weird for now
    70  	// we always want non-c-style after formatting
    71  	for _, line := range comment.Lines {
    72  		v.P(`//`, cleanCommentLine(line))
    73  	}
    74  }
    75  
    76  func (v *baseVisitor) POptions(options ...*proto.Option) {
    77  	v.pOptions(false, options...)
    78  }
    79  
    80  // fieldType can be "" in case of enum field
    81  func (v *baseVisitor) pMessageOrEnumField(prefix string, fieldName string, fieldType string, fieldTag int, comment *proto.Comment, inlineComment *proto.Comment, options ...*proto.Option) {
    82  	if fieldType != "" {
    83  		fieldType = fieldType + " "
    84  	}
    85  	v.PComment(comment)
    86  	if len(options) == 0 {
    87  		v.PWithInlineComment(inlineComment, prefix, fieldType, fieldName, " = ", fieldTag, ";")
    88  		return
    89  	}
    90  	if len(options) == 1 {
    91  		o := options[0]
    92  		if isSingleValueLiteral(o.Constant) {
    93  			if source := o.Constant.SourceRepresentation(); source != "" {
    94  				v.PWithInlineComment(inlineComment, prefix, fieldType, fieldName, " = ", fieldTag, " [", o.Name, ` = `, source, "];")
    95  				return
    96  			}
    97  		}
    98  	}
    99  	v.P(prefix, fieldType, fieldName, " = ", fieldTag, " [")
   100  	v.In()
   101  	v.pOptions(true, options...)
   102  	v.Out()
   103  	v.PWithInlineComment(inlineComment, "];")
   104  }
   105  
   106  func (v *baseVisitor) pOptions(isFieldOption bool, options ...*proto.Option) {
   107  	if len(options) == 0 {
   108  		return
   109  	}
   110  	sort.Slice(options, func(i int, j int) bool { return options[i].Name < options[j].Name })
   111  	prefix := "option "
   112  	if isFieldOption {
   113  		prefix = ""
   114  	}
   115  	for i, o := range options {
   116  		suffix := ";"
   117  		if isFieldOption {
   118  			if len(options) > 1 && i != len(options)-1 {
   119  				suffix = ","
   120  			} else {
   121  				suffix = ""
   122  			}
   123  		}
   124  		v.PComment(o.Comment)
   125  		if isSingleValueLiteral(o.Constant) {
   126  			// SourceRepresentation() returns an empty string if the literal is empty
   127  			// if empty, we do not want to print the key or empty value
   128  			if source := o.Constant.SourceRepresentation(); source != "" {
   129  				v.PWithInlineComment(o.InlineComment, prefix, o.Name, ` = `, source, suffix)
   130  			}
   131  		} else if len(o.Constant.Array) > 0 { // both Array and OrderedMap should not be set simultaneously, need more followup with emicklei/proto
   132  			v.Failures = append(
   133  				v.Failures,
   134  				text.NewFailuref(o.Position, "INVALID_PROTOBUF", "top-level options should never be arrays, this should not compile with protoc"),
   135  			)
   136  		} else { // len(o.Constant.OrderedMap) > 0
   137  			v.P(prefix, o.Name, ` = {`)
   138  			v.In()
   139  			for _, namedLiteral := range o.Constant.OrderedMap {
   140  				v.pInnerLiteral(namedLiteral.Name, *namedLiteral.Literal, "")
   141  			}
   142  			v.Out()
   143  			v.PWithInlineComment(o.InlineComment, `}`, suffix)
   144  		}
   145  	}
   146  }
   147  
   148  // should only be called by pOptions
   149  func (v *baseVisitor) pInnerLiteral(name string, literal proto.Literal, suffix string) {
   150  	prefix := ""
   151  	if name != "" {
   152  		prefix = name + ": "
   153  	}
   154  	if isSingleValueLiteral(literal) {
   155  		// SourceRepresentation() returns an empty string if the literal is empty
   156  		// if empty, we do not want to print the key or empty value
   157  		if source := literal.SourceRepresentation(); source != "" {
   158  			v.P(prefix, source, suffix)
   159  		}
   160  	} else if len(literal.Array) > 0 { // both Array and OrderedMap should not be set simultaneously, need more followup with emicklei/proto
   161  		v.P(prefix, `[`)
   162  		v.In()
   163  		for i, iLiteral := range literal.Array {
   164  			iSuffix := ""
   165  			if len(literal.Array) > 1 && i != len(literal.Array)-1 {
   166  				iSuffix = ","
   167  			}
   168  			v.pInnerLiteral("", *iLiteral, iSuffix)
   169  		}
   170  		v.Out()
   171  		v.P(`]`, suffix)
   172  	} else { // len(literal.OrderedMap) > 0
   173  		v.P(prefix, `{`)
   174  		v.In()
   175  		for _, namedLiteral := range literal.OrderedMap {
   176  			v.pInnerLiteral(namedLiteral.Name, *namedLiteral.Literal, "")
   177  		}
   178  		v.Out()
   179  		v.P(`}`, suffix)
   180  	}
   181  }
   182  
   183  func (v *baseVisitor) PField(prefix string, fieldType string, field *proto.Field) {
   184  	v.pMessageOrEnumField(prefix, field.Name, fieldType, field.Sequence, field.Comment, field.InlineComment, field.Options...)
   185  }
   186  
   187  func isSingleValueLiteral(literal proto.Literal) bool {
   188  	// TODO: this is a good example of the reasoning for https://github.com/uber/prototool/issues/1
   189  	return len(literal.Array) == 0 && len(literal.OrderedMap) == 0
   190  }
   191  
   192  func (v *baseVisitor) PEnumField(element *proto.EnumField) {
   193  	if element.ValueOption == nil {
   194  		v.pMessageOrEnumField("", element.Name, "", element.Integer, element.Comment, element.InlineComment)
   195  		return
   196  	}
   197  	v.pMessageOrEnumField("", element.Name, "", element.Integer, element.Comment, element.InlineComment, element.ValueOption)
   198  }
   199  
   200  func cleanCommentLine(line string) string {
   201  	// TODO: this is not great
   202  	return strings.TrimLeft(line, "/")
   203  }