// Copyright 2017-2025 WIT.COM Inc. All rights reserved. // Use of this source code is governed by the GPL 3.0 package main import ( "fmt" "iter" "os" "regexp" "strings" sync "sync" "go.wit.com/log" ) // like 'goimport', but for .proto files var allTheLines *LinesScanner var allTheNewLines []string /* type EnumMessage struct { msgPB *FormatMsg all []Message } type StdMessage struct { msgPB *FormatMsg all []Message } type Message interface { name() string addMsg(Message) } */ func protoReformatComments(filename string) error { // read in the .proto file data, err := os.ReadFile(filename) if err != nil { log.Info("file read failed", filename, err) return err } var newfile string log.Info("filename", filename) alltest := makeLineIter(data) // gets the max vartype and varname for line := range alltest { newfile += fmt.Sprintln(commentPreprocessor(line)) } newfile = commentPreprocessorFull(newfile) saveFile(filename, newfile) return nil } func protoReformat(filename string) error { // read in the .proto file data, err := os.ReadFile(filename) if err != nil { log.Info("file read failed", filename, err) return err } var newfile string basemsg := doParse(strings.Split(string(data), "\n")) for _, newline := range basemsg.format() { newfile += fmt.Sprintln(newline) } return saveFile(filename, newfile) } func doParse(lines []string) *FormatMsg { var comments string var inMessage bool var basemsg *FormatMsg basemsg = new(FormatMsg) inMessage = false // write out the messages allTheLines = newLinesScanner(lines) for allTheLines.Scan() { line := allTheLines.NextRaw() if strings.HasPrefix(line, "oneof ") { inMessage = true comments = "" if strings.Contains(line, "}") { newmsg := basemsg.newMessage(line, comments, FormatMsg_ONEOF) newmsg.Footer = "} // blah" continue } newmsg := basemsg.newMessage(line, comments, FormatMsg_ONEOF) newmsg.load() continue } if strings.HasPrefix(line, "enum ") { newmsg := basemsg.newMessage(line, comments, FormatMsg_ENUM) comments = "" inMessage = true if strings.Contains(line, "}") { newmsg.Footer = "} // blah" continue } newmsg.load() continue } if strings.HasPrefix(line, "message ") { log.Info("got to message", line) newmsg := basemsg.newMessage(line, comments, FormatMsg_MESSAGE) comments = "" inMessage = true if strings.Contains(line, "}") { newmsg.Footer = "} // blah" continue } newmsg.load() continue } if inMessage { comments += fmt.Sprintln(line) } else { basemsg.Notes = append(basemsg.Notes, line) } } return basemsg } func saveFile(filename string, data string) error { pf, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { log.Info("file open error. permissions?", filename, err) return err } data = strings.TrimSpace(data) fmt.Fprintln(pf, data) pf.Close() // for i, s := range slices.Backward(pf.ToSort) { return nil } func newDepth(fmtmsg *FormatMsg, header string) *FormatMsg { newmsg := new(FormatMsg) newmsg.MaxVarname = fmtmsg.MaxVarname newmsg.MaxVartype = fmtmsg.MaxVartype newmsg.Header = strings.TrimSpace(header) newmsg.Depth = fmtmsg.Depth + 1 return newmsg } func (msgPB *FormatMsg) newMessage(header string, comments string, msgType FormatMsg_Type) *FormatMsg { newmsg := newDepth(msgPB, header) newmsg.Type = msgType msgPB.Msgs = append(msgPB.Msgs, newmsg) comments = strings.TrimSpace(comments) newmsg.Notes = strings.Split(comments, "\n") return newmsg } func (msg *FormatMsg) load() { // fmtmsg := msg.msgPB for allTheLines.Scan() { line := allTheLines.Next() if strings.HasPrefix(line, "oneof ") { newmsg := msg.newMessage(line, "", FormatMsg_ONEOF) if strings.Contains(line, "}") { return } newmsg.load() continue } if strings.HasPrefix(line, "enum ") { newmsg := msg.newMessage(line, "", FormatMsg_ENUM) if strings.Contains(line, "}") { return } newmsg.load() continue } if strings.HasPrefix(line, "message ") { // message inception. search for the architect. don't forget your totem newmsg := msg.newMessage(line, "", FormatMsg_MESSAGE) if strings.Contains(line, "}") { return } newmsg.load() continue } if strings.HasPrefix(line, "}") { msg.Footer = line return } msg.Lines = append(msg.Lines, line) } return } // returns vartype, varname, id, end func tokenMsgVar(line string) (string, string, string, string) { parts := strings.Split(line, ";") front := parts[0] end := strings.Join(parts[1:], ";") var id string var varname string var vartype string parts = strings.Fields(front) parts, id = slicesPop(parts) parts, _ = slicesPop(parts) // this is the "=" sign parts, varname = slicesPop(parts) vartype = strings.Join(parts, " ") return vartype, varname, id, end } func slicesPop(parts []string) ([]string, string) { if len(parts) == 0 { return nil, "" } if len(parts) == 1 { return nil, parts[0] } x := len(parts) end := parts[x-1] return parts[0 : x-1], end } // 'for x := range' syntax using the smartly done golang 1.24 'iter' func makeLineIter(data []byte) iter.Seq[string] { items := strings.Split(string(data), "\n") // log.Println("Made All() Iter.Seq[] with length", len(items)) return func(yield func(string) bool) { for _, v := range items { if !yield(v) { return } } } } // find the max length of varname and vartype func setMaxSizes(curmsg *FormatMsg) { for _, line := range curmsg.Lines { parts := strings.Split(line, ";") if len(parts) < 2 { // line is blank or just a comment continue } vartype, varname, _, _ := tokenMsgVar(line) if len(vartype) > int(curmsg.MaxVartype) { curmsg.MaxVartype = int64(len(vartype)) } if len(varname) > int(curmsg.MaxVarname) { curmsg.MaxVarname = int64(len(varname)) } } } // use this for header and footer lines func (msg *FormatMsg) padBase() string { var pad string for i := 1; i < int(msg.Depth); i += 1 { pad += fmt.Sprintf("%8s", " ") } return pad } // use this for lines inside the message func (msg *FormatMsg) pad() string { var pad string for i := 0; i < int(msg.Depth); i += 1 { pad += fmt.Sprintf("%8s", " ") } return pad } func (msg *FormatMsg) padding(offset int) string { var pad string for i := offset; i < int(msg.Depth); i += 1 { pad += fmt.Sprintf("%8s", " ") } return pad } func formatEnum(curmsg *FormatMsg) []string { var newmsg []string newmsg = append(newmsg, curmsg.formatLineBase(curmsg.Header, "enum header")) for _, line := range curmsg.Lines { newmsg = append(newmsg, curmsg.formatLine(line, "enum")) } newmsg = append(newmsg, curmsg.formatLineBase(curmsg.Footer, "enum header")) return newmsg } // set all children to have the same max sizes func (parent *FormatMsg) formatStandardSizes() { var bigType int64 var bigName int64 // find the biggest var names and var types for _, child := range parent.Msgs { switch child.Type { case FormatMsg_ENUM: case FormatMsg_MESSAGE: // find the max length of varname and vartype setMaxSizes(child) if bigType < child.MaxVartype { bigType = child.MaxVartype } if bigName < child.MaxVarname { bigName = child.MaxVarname } default: } } // set this size in each message for _, child := range parent.Msgs { switch child.Type { case FormatMsg_ENUM: case FormatMsg_MESSAGE: child.MaxVartype = bigType child.MaxVarname = bigName default: } } } func (parent *FormatMsg) format() []string { parent.formatStandardSizes() switch parent.Type { case FormatMsg_ENUM: return formatEnum(parent) case FormatMsg_ONEOF: return formatEnum(parent) case FormatMsg_MESSAGE: return formatMessage(parent) default: return formatMessage(parent) } } func (msg *FormatMsg) formatLineBase(line string, dbg string) string { line = strings.TrimSpace(line) if argv.Debug { return fmt.Sprintf("/*a*/%s/*b*/%s // %s depth=%d", msg.padBase(), line, dbg, msg.Depth) } return fmt.Sprintf("%s%s", msg.padBase(), line) } func (msg *FormatMsg) formatLine(line string, dbg string) string { line = strings.TrimSpace(line) if argv.Debug { return fmt.Sprintf("/*a*/%s/*b*/%s // %s depth=%d", msg.pad(), line, dbg, msg.Depth) } return fmt.Sprintf("%s%s", msg.pad(), line) } func (msg *FormatMsg) formatComment(line string, dbg string) string { line = strings.TrimSpace(line) pad := fmt.Sprintf("%d", msg.MaxVartype+msg.MaxVarname+13) // 21 is correct? hmm := "%" + pad + "s %s" comment := fmt.Sprintf(hmm, " ", line) // todo: compute 50 if argv.Debug { return fmt.Sprintf("/*a*/%s/*b*/%s // %s depth=%d", msg.pad(), comment, dbg, msg.Depth) } return fmt.Sprintf("%s%s", msg.pad(), comment) } func (msg *FormatMsg) formatVarLine(line string, dbg string) string { line = strings.TrimSpace(line) mt := fmt.Sprintf("%d", msg.MaxVartype) mv := fmt.Sprintf("%d", msg.MaxVarname) hmm := "%-" + mt + "s %-" + mv + "s = %-3s %s" vartype, varname, id, end := tokenMsgVar(line) end = strings.TrimSpace(end) id = id + ";" newline := fmt.Sprintf(hmm, vartype, varname, id, end) newline = strings.TrimRight(newline, " ") if argv.Debug { return fmt.Sprintf("/*a*/%s/*b*/%s // %s depth=%d", msg.pad(), newline, dbg, msg.Depth) } return fmt.Sprintf("%s%s", msg.pad(), newline) } func trimLines(lines []string) []string { return strings.Split(strings.TrimSuffix(strings.Join(lines, "\n"), "\n"), "\n") } func formatMessage(curmsg *FormatMsg) []string { var newmsg []string // add the notes & comments before the header // newmsg = append(newmsg, strings.TrimSpace(strings.Join(curmsg.Notes, "\n"))) for _, line := range trimLines(curmsg.Notes) { newmsg = append(newmsg, curmsg.formatLineBase(line, "notes")) } if curmsg.Header != "" { var line string line = curmsg.formatLineBase(curmsg.Header, "header") parts := strings.Fields(line) if len(parts) > 3 { // hack to actually indent comments on the message line itself. you're welcome start := parts[0] + " " + parts[1] + " " + parts[2] end := strings.Join(parts[3:], " ") offset := int(curmsg.MaxVarname) + int(curmsg.MaxVartype) + 16 - len(start) pad := fmt.Sprintf("%d", offset) if argv.Debug { hmm := "%s %" + pad + "s %s // depth=%d" line = fmt.Sprintf(hmm, start, " ", end, curmsg.Depth) } else { hmm := "%s %" + pad + "s %s" line = fmt.Sprintf(hmm, start, " ", end) } newmsg = append(newmsg, line) // " //header") } else { newmsg = append(newmsg, line) // " //header") } } else { if curmsg.Depth != 0 { newmsg = append(newmsg, "// ERROR: header was blank") } } for _, msg := range curmsg.Msgs { switch msg.Type { case FormatMsg_ENUM: for _, line := range formatEnum(msg) { newmsg = append(newmsg, line) } case FormatMsg_ONEOF: for _, line := range formatEnum(msg) { newmsg = append(newmsg, line) } case FormatMsg_MESSAGE: for _, line := range msg.format() { // line = fmt.Sprintf("%s%s", curmsg.pad(), line) newmsg = append(newmsg, line) } default: } } for _, line := range curmsg.Lines { line = strings.TrimSpace(line) if line == "" { // newmsg = append(newmsg, line) newmsg = append(newmsg, "\n") continue } if strings.HasPrefix(line, "//") { /* pad := fmt.Sprintf("%d", curmsg.MaxVartype+curmsg.MaxVarname+21) hmm := "%" + pad + "s %s" line = fmt.Sprintf(hmm, " ", line) // todo: compute 50 newmsg = append(newmsg, line) */ newmsg = append(newmsg, curmsg.formatComment(line, "comment")) continue } newmsg = append(newmsg, curmsg.formatVarLine(line, "var")) continue /* mt := fmt.Sprintf("%d", curmsg.MaxVartype) mv := fmt.Sprintf("%d", curmsg.MaxVarname) hmm := "%s%-" + mt + "s %-" + mv + "s = %-3s %s" vartype, varname, id, end := tokenMsgVar(line) end = strings.TrimSpace(end) id = id + ";" var newline string if argv.Debug { newline = fmt.Sprintf(hmm+" //depth=%d", curmsg.padding(0), vartype, varname, id, end, curmsg.Depth) } else { newline = fmt.Sprintf(hmm, curmsg.padding(0), vartype, varname, id, end) } newline = strings.TrimRight(newline, " ") newmsg = append(newmsg, newline) */ } if curmsg.Footer == "" { newmsg = append(newmsg, "// footer was empty") } else { // newline := fmt.Sprintf("%s%s", curmsg.padding(1), curmsg.Footer) // +" //footer") // newmsg = append(newmsg, newline) newmsg = append(newmsg, curmsg.formatLineBase(curmsg.Footer, "msg depth")) } return newmsg } // DEFINE THE Lines ITERATOR. // itializes a new iterator. func newLinesScanner(things []string) *LinesScanner { return &LinesScanner{things: things} } type LinesScanner struct { sync.Mutex things []string index int } func (it *LinesScanner) Scan() bool { if it.index >= len(it.things) { return false } it.Lock() it.index++ it.Unlock() return true } // does no cleaning of the data func (it *LinesScanner) NextRaw() string { if it.index-1 == len(it.things) { fmt.Println("Next() error in LinesScanner", it.index) } return it.things[it.index-1] } // trims whitespace func (it *LinesScanner) Next() string { if it.index-1 == len(it.things) { fmt.Println("Next() error in LinesScanner", it.index) } // out := commentPreprocessor(it.things[it.index-1]) out := it.things[it.index-1] return strings.TrimSpace(out) // return out } // END DEFINE THE ITERATOR // turns: "/* test */ reserved /* linkPreviews */ 4;" // into: reserved 1; // test // linkPreviews func commentPreprocessor(line string) string { // Match all /* comment */ blocks re := regexp.MustCompile(`/\*([^*]+)\*/`) matches := re.FindAllStringSubmatch(line, -1) // Extract just the comment texts var comments []string for _, match := range matches { comments = append(comments, strings.TrimSpace(match[1])) // comments = append(comments, match[1]) } // Remove the block comments from the original line line = re.ReplaceAllString(line, "") // line = strings.TrimSpace(line) line = strings.TrimSuffix(line, " ") // Append comments at the end with // for _, comment := range comments { line += " // " + comment } return line } // /* this // - thing // */ // // becomes // // this // thing func commentPreprocessorFull(full string) string { // Match all /* comment */ blocks // re := regexp.MustCompile(`/\*([^*]+)\*/`) re := regexp.MustCompile(`(?s)/\*(.*?)\*/`) return re.ReplaceAllStringFunc(full, func(s string) string { log.Info("FOUND:\n", s) lines := strings.Split(s, "\n") var cleaned []string for _, line := range lines { trimmed := strings.TrimSpace(line) switch { case strings.HasPrefix(trimmed, "/*"): trimmed = trimCommentPrefix(trimmed) case strings.HasPrefix(trimmed, "*/"): trimmed = strings.TrimPrefix(trimmed, "*/") case strings.HasPrefix(trimmed, "*"): trimmed = strings.TrimPrefix(trimmed, "*") } trimmed = "// " + trimmed cleaned = append(cleaned, strings.TrimSpace(trimmed)) } s = strings.Join(cleaned, "\n") log.Info("NOW:\n", s) return s }) } func trimCommentPrefix(line string) string { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "/") { i := 1 for i < len(trimmed) && trimmed[i] == '*' { i++ } if i > 1 { return strings.TrimSpace(trimmed[i:]) } } if strings.HasPrefix(trimmed, "*") { return strings.TrimSpace(trimmed[1:]) } if trimmed == "*/" { return "" } return trimmed }