| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package formatter |
| |
| import ( |
| "bytes" |
| "fmt" |
| "mojom/mojom_parser/lexer" |
| "mojom/mojom_parser/mojom" |
| "sort" |
| "strings" |
| ) |
| |
| // printer is a single-use struct that keeps track of the state necessary to |
| // pretty-print a mojom file. |
| // It is used by FormatMojom (see formatter.go) |
| // |
| // Internall, the entry point is writeMojomFile and the result is obtained by |
| // calling result. |
| // |
| // The pretty-printing of individual elements is implemented using the write.* |
| // methods. Those methods accept the element to be printed (or the element |
| // of which a portion is to be printed) and write the pretty-printed output |
| // to |buffer|. |
| type printer struct { |
| // used indicates whether the printer has been used or not. If set to true, |
| // writeMojomFile will panic. |
| used bool |
| |
| // buffer is the buffer to which the write.* methods write the pretty-printed |
| // mojom file element. |
| buffer bytes.Buffer |
| |
| // indentLevel is the current depth of indentation. |
| indentLevel int |
| |
| // eolComment is the comment to be printed at the end of the current line. |
| eolComment *lexer.Token |
| |
| // lineStarted is false if nothing has been written on the current line yet. |
| lineStarted bool |
| } |
| |
| // newPrinter is a constructor for printer. |
| func newPrinter() (p *printer) { |
| p = new(printer) |
| return |
| } |
| |
| // result returns the pretty-printed version of what was given to the printer. |
| // Usually, this is a pretty-printed MojomFile. |
| func (p *printer) result() string { |
| return p.buffer.String() |
| } |
| |
| // writeMojomFile is the entry point of the pretty printer. |
| // It takes a mojom file with attached comments and creates a pretty-printed |
| // representation. |
| func (p *printer) writeMojomFile(mojomFile *mojom.MojomFile) { |
| if p.used { |
| panic("A printer can only be used once!") |
| } |
| p.used = true |
| |
| p.writeAttributes(mojomFile.Attributes) |
| p.writeModuleNamespace(mojomFile.ModuleNamespace) |
| |
| if len(mojomFile.Imports) > 0 { |
| p.nl() |
| p.writeImportedFiles(mojomFile.Imports) |
| } |
| |
| for i, declaredObject := range mojomFile.DeclaredObjects { |
| p.writeDeclaredObject(declaredObject) |
| if i < len(mojomFile.DeclaredObjects)-1 { |
| p.nl() |
| } |
| } |
| |
| if mojomFile.FinalComments != nil { |
| p.nl() |
| p.nl() |
| p.writeCommentBlocks(mojomFile.FinalComments) |
| } |
| } |
| |
| // writeModuleNamespace writes a mojom file's module statement and associated |
| // comments. |
| func (p *printer) writeModuleNamespace(module *mojom.ModuleNamespace) { |
| p.writeBeforeComments(module) |
| p.writef("module %s;", module.Identifier) |
| p.writeRightComments(module) |
| p.nl() |
| } |
| |
| // writeImportedFiles sorts and writes a slice of import statements. |
| func (p *printer) writeImportedFiles(imports []*mojom.ImportedFile) { |
| // TODO(azani): Detect and handle separate blocks of imports. |
| sortImportedFiles(imports) |
| for _, imported := range imports { |
| p.writeBeforeComments(imported) |
| p.writef("import \"%v\";", imported.SpecifiedName) |
| p.writeRightComments(imported) |
| p.nl() |
| } |
| } |
| |
| // writeDeclaredObject writes a declared object's attributes, then its |
| // preceeding comments and finally the object itself. |
| func (p *printer) writeDeclaredObject(declaredObject mojom.DeclaredObject) { |
| if isNewBlock(declaredObject) { |
| p.nl() |
| } |
| p.writeAttributes(declaredObject.Attributes()) |
| p.writeBeforeComments(declaredObject) |
| switch o := declaredObject.(type) { |
| case *mojom.MojomStruct: |
| p.writeMojomStruct(o) |
| case *mojom.StructField: |
| p.writeStructField(o) |
| case *mojom.MojomUnion: |
| p.writeMojomUnion(o) |
| case *mojom.UnionField: |
| p.writeUnionField(o) |
| case *mojom.MojomEnum: |
| p.writeMojomEnum(o) |
| case *mojom.EnumValue: |
| p.writeEnumValue(o) |
| case *mojom.UserDefinedConstant: |
| p.writeUserDefinedConstant(o) |
| case *mojom.MojomInterface: |
| p.writeMojomInterface(o) |
| case *mojom.MojomMethod: |
| p.writeMojomMethod(o) |
| default: |
| panic(fmt.Sprintf("writeDeclaredObject cannot write %v.", declaredObject)) |
| } |
| } |
| |
| func (p *printer) writeMojomStruct(mojomStruct *mojom.MojomStruct) { |
| p.write("struct ") |
| p.writeDeclaredObjectsContainer(mojomStruct) |
| } |
| |
| func (p *printer) writeMojomUnion(mojomUnion *mojom.MojomUnion) { |
| p.write("union ") |
| p.writeDeclaredObjectsContainer(mojomUnion) |
| } |
| |
| func (p *printer) writeMojomEnum(mojomEnum *mojom.MojomEnum) { |
| p.write("enum ") |
| p.writeDeclaredObjectsContainer(mojomEnum) |
| } |
| |
| func (p *printer) writeMojomInterface(mojomInterface *mojom.MojomInterface) { |
| p.write("interface ") |
| p.writeDeclaredObjectsContainer(mojomInterface) |
| } |
| |
| func (p *printer) writeMojomMethod(mojomMethod *mojom.MojomMethod) { |
| p.write(mojomMethod.NameToken().Text) |
| p.writeMethodParams(mojomMethod.Parameters) |
| if mojomMethod.ResponseParameters != nil && len(mojomMethod.ResponseParameters.Fields) > 0 { |
| p.write(" => ") |
| p.writeMethodParams(mojomMethod.ResponseParameters) |
| } |
| p.write(";") |
| p.writeRightComments(mojomMethod) |
| } |
| |
| // writeMethodParams writes the pretty-printed method parameters represented by |
| // a MojomStruct. |
| func (p *printer) writeMethodParams(params *mojom.MojomStruct) { |
| p.write("(") |
| declaredObjects := params.GetDeclaredObjects() |
| for i, param := range declaredObjects { |
| p.writeMethodParam(param.(*mojom.StructField)) |
| if i < len(declaredObjects)-1 { |
| p.write(", ") |
| } |
| } |
| |
| p.write(")") |
| } |
| |
| // writeMethodParam writes a single pretty-printed method parameter represented |
| // by a StructField. |
| func (p *printer) writeMethodParam(param *mojom.StructField) { |
| p.writeAttributes(param.Attributes()) |
| p.writeBeforeComments(param) |
| p.writeTypeRef(param.FieldType) |
| p.writef(" %v", param.NameToken().Text) |
| if param.DefaultValue != nil { |
| p.write("=") |
| p.writeValueRef(param.DefaultValue) |
| } |
| p.writeRightComments(param) |
| } |
| |
| func (p *printer) writeUserDefinedConstant(constant *mojom.UserDefinedConstant) { |
| p.write("const ") |
| p.writeTypeRef(constant.DeclaredType()) |
| p.writef(" %s", constant.NameToken().Text) |
| p.write(" = ") |
| p.writeValueRef(constant.ValueRef()) |
| p.write(";") |
| p.writeRightComments(constant) |
| } |
| |
| func (p *printer) writeStructField(structField *mojom.StructField) { |
| p.writeTypeRef(structField.FieldType) |
| p.writef(" %v", structField.NameToken().Text) |
| if structField.DeclaredOrdinal() >= 0 { |
| p.writef("@%v", structField.DeclaredOrdinal()) |
| } |
| if structField.DefaultValue != nil { |
| p.write(" = ") |
| p.writeValueRef(structField.DefaultValue) |
| } |
| p.write(";") |
| p.writeRightComments(structField) |
| } |
| |
| func (p *printer) writeUnionField(unionField *mojom.UnionField) { |
| p.writeTypeRef(unionField.FieldType) |
| p.writef(" %v", unionField.NameToken().Text) |
| if unionField.DeclaredOrdinal() >= 0 { |
| p.writef("@%v", unionField.DeclaredOrdinal()) |
| } |
| p.write(";") |
| p.writeRightComments(unionField) |
| } |
| |
| func (p *printer) writeEnumValue(enumValue *mojom.EnumValue) { |
| p.write(enumValue.NameToken().Text) |
| if enumValue.ValueRef() != nil { |
| p.write(" = ") |
| p.writeValueRef(enumValue.ValueRef()) |
| } |
| p.write(",") |
| } |
| |
| // writeDeclaredObjectsContainer |
| // DeclaredObjectsContainers are MojomEnum, MojomUnion, MojomInterface, MojomStruct. |
| // This method writes the name of the object, an opening brace, the contained |
| // declarations (fields, enum values, nested declarations and methods) and the |
| // closing brace. |
| func (p *printer) writeDeclaredObjectsContainer(container mojom.DeclaredObjectsContainer) { |
| // TODO(azani): Detect when a blank line should separate two declared objects. |
| p.writef("%v {", container.(mojom.DeclaredObject).NameToken().Text) |
| p.writeRightComments(container.(mojom.MojomElement)) |
| p.incIndent() |
| p.nl() |
| declaredObjects := container.GetDeclaredObjects() |
| for i, declaredObject := range declaredObjects { |
| p.writeDeclaredObject(declaredObject) |
| if i < len(declaredObjects)-1 { |
| p.nl() |
| } |
| } |
| |
| p.decIndent() |
| p.nl() |
| p.write("};") |
| } |
| |
| func (p *printer) writeAttributes(attrs *mojom.Attributes) { |
| if attrs == nil || len(attrs.List) == 0 { |
| return |
| } |
| |
| p.writef("[") |
| for idx, attr := range attrs.List { |
| p.writeAttribute(&attr) |
| if idx < len(attrs.List)-1 { |
| p.writef(",") |
| p.nl() |
| } |
| } |
| p.writef("]") |
| } |
| |
| func (p *printer) writeAttribute(attr *mojom.MojomAttribute) { |
| p.writef("%s=", attr.Key) |
| p.writeValueRef(attr.Value) |
| } |
| |
| func (p *printer) writeTypeRef(t mojom.TypeRef) { |
| switch t.TypeRefKind() { |
| case mojom.TypeKindUserDefined: |
| u := t.(*mojom.UserTypeRef) |
| p.write(u.Identifier()) |
| if u.Nullable() { |
| p.write("?") |
| } |
| case mojom.TypeKindArray: |
| a := t.(*mojom.ArrayTypeRef) |
| p.write("array<") |
| p.writeTypeRef(a.ElementType()) |
| if a.FixedLength() >= 0 { |
| p.writef(", %v", a.FixedLength()) |
| } |
| p.write(">") |
| if a.Nullable() { |
| p.write("?") |
| } |
| case mojom.TypeKindMap: |
| m := t.(*mojom.MapTypeRef) |
| p.write("map<") |
| p.writeTypeRef(m.KeyType()) |
| p.write(", ") |
| p.writeTypeRef(m.ValueType()) |
| p.write(">") |
| if m.Nullable() { |
| p.write("?") |
| } |
| case mojom.TypeKindHandle: |
| fallthrough |
| case mojom.TypeKindSimple: |
| fallthrough |
| case mojom.TypeKindString: |
| p.write(t.String()) |
| default: |
| panic("This unhandled TypeRefKind: " + t.String()) |
| } |
| } |
| |
| func (p *printer) writeValueRef(value mojom.ValueRef) { |
| switch v := value.(type) { |
| case mojom.LiteralValue: |
| p.write(v.String()) |
| case *mojom.UserValueRef: |
| p.write(v.Identifier()) |
| default: |
| panic("Cannot handle this value ref.") |
| } |
| } |
| |
| // writeBeforeComments writes the comments preceeding a MojomElement. |
| func (p *printer) writeBeforeComments(el mojom.MojomElement) { |
| attachedComments := el.AttachedComments() |
| if attachedComments == nil { |
| return |
| } |
| |
| p.writeAboveComments(el) |
| p.writeLeftComments(el) |
| } |
| |
| // writeAboveComments writes the comments above of a MojomElement. |
| func (p *printer) writeAboveComments(el mojom.MojomElement) { |
| attachedComments := el.AttachedComments() |
| if attachedComments == nil { |
| return |
| } |
| |
| p.writeCommentBlocks(attachedComments.Above) |
| } |
| |
| // writeLeftComments writes the comments left of a MojomElement. |
| func (p *printer) writeLeftComments(el mojom.MojomElement) { |
| attachedComments := el.AttachedComments() |
| if attachedComments == nil { |
| return |
| } |
| |
| for _, comment := range attachedComments.Left { |
| if comment.Kind == lexer.SingleLineComment { |
| panic("SingleLineComment cannot be on the left of an element.") |
| } |
| |
| p.writef("%s ", comment.Text) |
| } |
| } |
| |
| // writeRightComments writes the comments to the right of a MojomElement. |
| func (p *printer) writeRightComments(el mojom.MojomElement) { |
| attachedComments := el.AttachedComments() |
| if attachedComments == nil { |
| return |
| } |
| |
| for i, comment := range attachedComments.Right { |
| if comment.Kind == lexer.SingleLineComment { |
| if i < len(attachedComments.Right)-1 { |
| panic("You can't have anything after a SingleLineComment!") |
| } |
| p.setEolComment(comment) |
| break |
| } |
| p.writef(" %s", comment.Text) |
| } |
| |
| } |
| |
| func (p *printer) writeCommentBlocks(comments []lexer.Token) { |
| for i, comment := range comments { |
| switch comment.Kind { |
| case lexer.SingleLineComment: |
| p.write(comment.Text) |
| p.nl() |
| case lexer.MultiLineComment: |
| p.writeMultiLineComment(comment) |
| case lexer.EmptyLine: |
| // We only use EmptyLine here to separate comment blocks. And we collapse |
| // sequences of empty lines to a single empty line. |
| if i != 0 && comments[i-1].Kind != lexer.EmptyLine { |
| p.nl() |
| } |
| default: |
| panic(fmt.Sprintf("%s is not a comment.", comment)) |
| } |
| } |
| } |
| |
| func (p *printer) writeMultiLineComment(comment lexer.Token) { |
| if comment.Kind != lexer.MultiLineComment { |
| panic(fmt.Sprintf("This is not a MultiLineComment: %s", comment)) |
| } |
| |
| for i, line := range strings.Split(comment.Text, "\n") { |
| if i != 0 { |
| p.write(" ") |
| } |
| p.write(strings.Trim(line, " ")) |
| p.nl() |
| } |
| } |
| |
| // Utility functions |
| |
| // writef writes according to the format specifier. See fmt.Printf for the |
| // format specifier. |
| func (p *printer) writef(format string, a ...interface{}) { |
| p.write(fmt.Sprintf(format, a...)) |
| } |
| |
| // write writes the provided string to the buffer. |
| // If nothing has been written yet on the current line write also writes the |
| // current indentation level. |
| func (p *printer) write(s string) { |
| if strings.ContainsRune(s, '\n') { |
| panic(fmt.Sprintf("Only the nl method can write a new line: %q", s)) |
| } |
| |
| // We only print the indentation if the line is not empty. |
| if !p.lineStarted { |
| p.buffer.WriteString(strings.Repeat(" ", p.indentLevel)) |
| p.lineStarted = true |
| } |
| p.buffer.WriteString(s) |
| } |
| |
| // nl writes a new line. Before writing the new line, nl writes the last |
| // comment on the line. |
| func (p *printer) nl() { |
| // Before going to the next line, print the last comment on the line. |
| if p.eolComment != nil { |
| p.writef(" %s", p.eolComment.Text) |
| p.eolComment = nil |
| } |
| |
| p.buffer.WriteString("\n") |
| p.lineStarted = false |
| } |
| |
| func (p *printer) incIndent() { |
| p.indentLevel += 1 |
| } |
| |
| func (p *printer) decIndent() { |
| if p.indentLevel == 0 { |
| panic("The printer is attempting to use negative indentation!") |
| } |
| p.indentLevel -= 1 |
| } |
| |
| // setEolComment sets the comment that is to be printed at the end of the current line. |
| // The last comment on the line is not necessarily associated with the last |
| // element on the line. So when the last comment is found, we record that |
| // comment so we may print it right before going to the next line. |
| func (p *printer) setEolComment(comment lexer.Token) { |
| if p.eolComment != nil { |
| panic("There is space for only one comment at the end of the line!") |
| } |
| |
| if comment.Kind != lexer.SingleLineComment { |
| panic("Only SingleLineComments need to be handled specially at the end of line.") |
| } |
| |
| p.eolComment = &comment |
| } |
| |
| // Standalone utilities that do not operate on the buffer. |
| |
| // isNewBlock determines if an empty line should be printed above the element |
| // under consideration. The purpose is to respect user-intention to separate |
| // elements in a mojom file. |
| // |
| // If the element has attributes, and the first "comment" attached above the |
| // attributes is an empty line, the element is a new block. |
| // If the element has no attributes and the first "comment" attached above the |
| // element itself is an empty line, the element is a new block. |
| // Else, the element is not a new block. |
| func isNewBlock(declaredObject mojom.DeclaredObject) bool { |
| if declaredObject.Attributes() != nil { |
| comments := declaredObject.Attributes().AttachedComments() |
| if comments == nil || len(comments.Above) == 0 { |
| return false |
| } |
| return comments.Above[0].Kind == lexer.EmptyLine |
| } |
| comments := declaredObject.AttachedComments() |
| |
| if comments == nil || len(comments.Above) == 0 { |
| return false |
| } |
| return comments.Above[0].Kind == lexer.EmptyLine |
| } |
| |
| // Following is a utility to sort slices of |ImportedFile|s. |
| |
| // sortImportedFiles sorts the slice of imported files it receives. |
| func sortImportedFiles(imports []*mojom.ImportedFile) { |
| sort.Sort(&importedFilesSorter{imports}) |
| } |
| |
| type importedFilesSorter struct { |
| imports []*mojom.ImportedFile |
| } |
| |
| // See sort.Interface. |
| func (ifs *importedFilesSorter) Len() int { |
| return len(ifs.imports) |
| } |
| |
| // See sort.Interface. |
| func (ifs *importedFilesSorter) Less(i, j int) bool { |
| return ifs.imports[i].SpecifiedName < ifs.imports[j].SpecifiedName |
| } |
| |
| // See sort.Interface. |
| func (ifs *importedFilesSorter) Swap(i, j int) { |
| ifs.imports[i], ifs.imports[j] = ifs.imports[j], ifs.imports[i] |
| } |