/* 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" ) 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 } func FindCommentBlocks(s *State, file string, output chan FileRawBlock, w *sync.WaitGroup) { fullPath := path.Join(s.ProjectRoot, file) readFile, err := os.Open(fullPath) if err != nil { s.ErrChannel <- err close(output) w.Done() return } ext := filepath.Ext(file) lang := s.GetLangForExt(ext) if lang == nil { close(output) w.Done() // log.Warnf("don't know about the language for extension '%s'", ext) return } 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) output <- FileRawBlock{block, lang} } block = RawCommentBlock{0, []string{}, &curFile} } line++ } if err = readFile.Close(); err != nil { close(output) w.Done() s.ErrChannel <- err return } w.Done() close(output) } func getBlockFirstLine(lang *Lang, block RawCommentBlock) *string { txt := strings.TrimLeft(block.Lines[0], *lang.CommentBlockMarker) txt = strings.TrimSpace(txt) return &txt } 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 } func processCommentBlocks(s *State, file chan FileRawBlock, output chan Block, w *sync.WaitGroup) { defer close(output) defer w.Done() // 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.ErrChannel <- e return } 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 } } func PrintResult(state *State, res chan Block, w *sync.WaitGroup) { for b := range res { tags := strings.Join(b.Tags, ",") fmt.Printf("* %s at %s:%d | Tags: %s\n%s\n%s\n\n", b.BlockType, *b.FilePath, b.Loc.Start, tags, b.Desc, b.WrappedCode) } w.Done() } func ProcessFiles(state *State, files chan string) error { var workers sync.WaitGroup for file := range files { fileBlocks := make(chan FileRawBlock, 10) processed := make(chan Block, 100) if file == "utils/bazel/zlib.bzl" { println("file: ", file) } workers.Add(3) go FindCommentBlocks(state, file, fileBlocks, &workers) go processCommentBlocks(state, fileBlocks, processed, &workers) go PrintResult(state, processed, &workers) } controller := make(chan bool) go func() { workers.Wait() close(controller) }() select { case <-controller: println("Wait done") case <-time.After(time.Second * 20): println("timeout") } state.WaitGroup.Done() return nil }