/* Copyright © 2022 Sameer Rahmani This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package core import ( "bufio" "fmt" "os" "path" "path/filepath" "sync" "time" "strings" log "github.com/sirupsen/logrus" ) // Start and End will indicate the of starting and ending // line number of a comment block and NOT the position type Location struct { Start int End int } type RawCommentBlock struct { LineNo int Lines []string File *File } type File struct { Path string Blocks []RawCommentBlock Lines []string } type Block struct { BlockType string // Like TODO, NOTE, etc FilePath *string Tags []string Desc string WrappedCode string Loc Location } type FileRawBlock struct { Block RawCommentBlock Lang *Lang } type CommentStack struct { storage []*Block } func (s *CommentStack) Push(block *Block) { s.storage = append(s.storage, block) } func (s *CommentStack) Pop() *Block { l := len(s.storage) if l == 0 { return nil } item := s.storage[l-1] s.storage = s.storage[:l-1] return item } func (b *RawCommentBlock) append(txt string) { b.Lines = append(b.Lines, txt) } func (b *RawCommentBlock) empty() bool { return len(b.Lines) == 0 } // Strip the first line of a comment block from comment markers and spaces func getBlockFirstLine(lang *Lang, block RawCommentBlock) *string { txt := strings.TrimLeft(block.Lines[0], *lang.CommentBlockMarker) txt = strings.TrimSpace(txt) return &txt } // Join the comment line to create paragharphs of readable text func JoinComments(state *State, commentMarker string, b RawCommentBlock) *string { commentLines := b.Lines firstLine := state.TrimTypeAndTags(&commentLines[0]) result := []string{} if firstLine != nil { result = append(result, *firstLine) } if len(commentLines) > 1 { for _, line := range (commentLines)[1:] { trimmed := strings.TrimSpace(strings.TrimLeft(line, commentMarker)) result = append(result, trimmed) } } txt := strings.Join(result, "\n") return &txt } // Process raw comment blocks to extract the type, tags and the content and write // the processed blocks to the output channel func processCommentBlocks(s *State, file chan FileRawBlock, output chan Block, w *sync.WaitGroup) { // To keep track of opening and closing tags var stack CommentStack for fileBlock := range file { block := fileBlock.Block lang := fileBlock.Lang if block.empty() { continue } txt := getBlockFirstLine(lang, block) type_ := s.getCommentType(*txt) if type_ == nil { type_, isClosing := s.detectClosingBlock(txt) if !isClosing { // A regular comment block, skipping continue } lastOpenBlock := stack.Pop() if lastOpenBlock == nil { e := fmt.Errorf( "closing comment with no Opening at '%s:%d'", block.File.Path, block.LineNo, ) s.CheckErr(e) continue } if type_ == &lastOpenBlock.BlockType { wrappedCode := []string{} for i := lastOpenBlock.Loc.End; i < block.LineNo; i++ { ithLine := (block.File.Lines)[i] wrappedCode = append(wrappedCode, ithLine) } lastOpenBlock.WrappedCode = strings.Join(wrappedCode, "\n") } continue } tags := s.getTags(*txt) processedBlock := Block{ BlockType: *type_, FilePath: &block.File.Path, Tags: tags, Desc: *JoinComments(s, *lang.CommentBlockMarker, block), WrappedCode: "", Loc: Location{block.LineNo, len(block.Lines) + block.LineNo}, } stack.Push(&processedBlock) output <- processedBlock } log.Debug("Terminating the block processor") close(output) w.Done() } func formatter(state *State, input chan Block, output chan string, w *sync.WaitGroup) { for b := range input { tags := strings.Join(b.Tags, ",") output <- fmt.Sprintf("* %s at %s:%d | Tags: %s\n%s\n%s\n\n", b.BlockType, *b.FilePath, b.Loc.Start, tags, b.Desc, b.WrappedCode) } log.Debug("Terminating the formatter") w.Done() } func ProcessFiles(state *State, files chan string, output chan string) { var workers sync.WaitGroup for i := 0; i < state.NumberOfWorkers; i++ { workers.Add(1) go Worker(state, files, output, i, &workers) } controller := make(chan bool) go func() { workers.Wait() close(controller) }() select { case <-controller: log.Debug("Workers are terminated correctly.") case <-time.After(time.Second * 20): log.Fatal(`Workers timout! This is a bug! This is probably due to the buffer size difference between formatter and the printer. Please file a bug.`) } close(output) state.WaitGroup.Done() } func Worker(state *State, input chan string, output chan string, workerNumber int, w *sync.WaitGroup) { log.Debugf("Spawning worker '%d'", workerNumber) fileBlocks := make(chan FileRawBlock, state.NumberOfWorkers) processed := make(chan Block, state.NumberOfWorkers) w.Add(2) go processCommentBlocks(state, fileBlocks, processed, w) go formatter(state, processed, output, w) for file := range input { fullPath := path.Join(state.ProjectRoot, file) readFile, err := os.Open(fullPath) if err != nil { state.CheckErr(err) continue } ext := filepath.Ext(file) lang := state.GetLangForExt(ext) if lang == nil { err := fmt.Errorf("don't know about the language for extension '%s'", ext) state.CheckErr(err) continue } scanner := bufio.NewScanner(readFile) scanner.Split(bufio.ScanLines) curFile := File{ Path: file, Blocks: []RawCommentBlock{}, Lines: []string{}, } var line = 1 block := RawCommentBlock{0, []string{}, &curFile} for scanner.Scan() { rawText := scanner.Text() curFile.Lines = append(curFile.Lines, rawText) txt := strings.TrimSpace(rawText) if strings.HasPrefix(txt, *lang.CommentBlockMarker) { if block.empty() { block.LineNo = line } block.append(txt) } else { if !block.empty() { curFile.Blocks = append(curFile.Blocks, block) fileBlocks <- FileRawBlock{block, lang} } block = RawCommentBlock{0, []string{}, &curFile} } line++ } if err = readFile.Close(); err != nil { state.ErrChannel <- err continue } } close(fileBlocks) log.Debugf("Worker '%d' terminated", workerNumber) w.Done() }