github.com/blaisereilly/goreporter@v0.0.0-20240129165232-a6e9a46234bd/engine/reporter.go (about)

     1  // Copyright 2017 The GoReporter Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //    http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package engine
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"io/ioutil"
    20  	"path/filepath"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/fatih/color"
    26  	"github.com/golang/glog"
    27  	"github.com/json-iterator/go"
    28  
    29  	"github.com/360EntSecGroup-Skylar/goreporter/utils"
    30  )
    31  
    32  type Synchronizer struct {
    33  	SyncRW                *sync.RWMutex     `inject:""`
    34  	WaitGW                *WaitGroupWrapper `inject:""`
    35  	LintersProcessChans   chan int64        `json:"-"`
    36  	LintersFinishedSignal chan string       `json:"-"`
    37  }
    38  
    39  // Reporter is the top struct of GoReporter.
    40  type Reporter struct {
    41  	Project   string            `json:"project"`
    42  	Score     int               `json:"score"`
    43  	Grade     int               `json:"grade"`
    44  	Metrics   map[string]Metric `json:"metrics"`
    45  	Issues    int               `json:"issues"`
    46  	TimeStamp string            `json:"time_stamp"`
    47  	Linters   []StrategyLinter
    48  	Sync      *Synchronizer `inject:"" json:"-"`
    49  
    50  	ProjectPath    string `json:"-"`
    51  	ReportPath     string `json:"-"`
    52  	HtmlTemplate   string `json:"-"`
    53  	ReportFormat   string `json:"-"`
    54  	ExceptPackages string `json:"-"`
    55  
    56  	StartTime time.Time
    57  }
    58  
    59  // WaitGroupWrapper is a struct that as a waiter for all linetr-tasks.And it
    60  // encapsulates Sync.WaitGroup that can be call as a interface.
    61  type WaitGroupWrapper struct {
    62  	sync.WaitGroup
    63  }
    64  
    65  // Wrap implements a interface that run the function cd as a goroutine.And it
    66  // encapsulates Add(1) and Done() operation.You can just think go cd() but not
    67  // worry about synchronization and security issues.
    68  func (w *WaitGroupWrapper) Wrap(cb func()) {
    69  	w.Add(1)
    70  	go func() {
    71  		cb()
    72  		w.Done()
    73  	}()
    74  }
    75  
    76  type StrategyParameter struct {
    77  	AllDirs, UnitTestDirs       map[string]string
    78  	ProjectPath, ExceptPackages string
    79  }
    80  
    81  // Report is a important function of goreporter, it will run all linters and rebuild
    82  // metrics data in a golang project. And all linters' result will be as one metric
    83  // data for Reporter.
    84  func (r *Reporter) Report() error {
    85  	defer r.Close()
    86  	glog.Infoln("start code quality assessment...")
    87  
    88  	r.Project = utils.PackageAbsPath(r.ProjectPath)
    89  
    90  	// All directory that has _test.go files will be add into.
    91  	dirsUnitTest, err := utils.DirList(r.ProjectPath, "_test.go", r.ExceptPackages)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	// All directory that has .go files will be add into.
    97  	dirsAll, err := utils.DirList(r.ProjectPath, ".go", r.ExceptPackages)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	params := StrategyParameter{
   103  		AllDirs:      dirsAll,
   104  		UnitTestDirs: dirsUnitTest,
   105  		ProjectPath:  r.ProjectPath,
   106  	}
   107  
   108  	for _, linter := range r.Linters {
   109  		r.compute(linter, params)
   110  	}
   111  
   112  	r.TimeStamp = time.Now().Format("2006-01-02-15-04-05")
   113  
   114  	// ensure peocessbar quit.
   115  	r.Sync.LintersProcessChans <- 100
   116  	glog.Infoln("finished code quality assessment...")
   117  	return nil
   118  }
   119  
   120  func (r *Reporter) compute(strategy StrategyLinter, params StrategyParameter) {
   121  	glog.Infof("running %s...", strategy.GetName())
   122  
   123  	summaries := strategy.Compute(params)
   124  
   125  	r.Metrics[strategy.GetName()+"Tips"] = Metric{
   126  		Name:        strategy.GetName(),
   127  		Description: strategy.GetDescription(),
   128  		Weight:      strategy.GetWeight(),
   129  		Summaries:   summaries.Summaries,
   130  		Percentage:  strategy.Percentage(summaries),
   131  	}
   132  
   133  	r.Sync.LintersFinishedSignal <- fmt.Sprintf("Linter:%s over,time consuming %vs", strategy.GetName(), time.Since(r.StartTime).Seconds())
   134  	glog.Infof("%s over!", strategy.GetName())
   135  }
   136  
   137  func (r *Reporter) Render() (err error) {
   138  	switch r.ReportFormat {
   139  	case "json":
   140  		err = r.toJson()
   141  	case "text":
   142  		err = r.toText()
   143  	default:
   144  		glog.Infof(fmt.Sprintf("Generating HTML report,time consuming %vs", time.Since(r.StartTime).Seconds()))
   145  		err = r.toHtml()
   146  		if err != nil {
   147  			glog.Infoln("Json2Html error:", err)
   148  			return
   149  		}
   150  	}
   151  	return
   152  }
   153  
   154  // toJson will marshal struct Reporter into json and
   155  // return a []byte data.
   156  func (r *Reporter) toJson() (err error) {
   157  	glog.Infof(fmt.Sprintf("Generating json report,time consuming %vs", time.Since(r.StartTime).Seconds()))
   158  	jsonReport, err := jsoniter.Marshal(r)
   159  	if err != nil {
   160  		return
   161  	}
   162  
   163  	saveAbsPath := utils.AbsPath(r.ReportPath)
   164  	projectName := utils.ProjectName(r.ProjectPath)
   165  
   166  	jsonpath := projectName + "-" + r.TimeStamp + ".json"
   167  	if saveAbsPath != "" && saveAbsPath != r.ReportPath {
   168  		jsonpath = strings.Replace(saveAbsPath+string(filepath.Separator)+projectName+"-"+r.TimeStamp+".json", string(filepath.Separator)+string(filepath.Separator), string(filepath.Separator), -1)
   169  	}
   170  	if err = ioutil.WriteFile(jsonpath, jsonReport, 0666); err != nil {
   171  		return
   172  	}
   173  	glog.Info("Json report saved in:", jsonpath)
   174  	return
   175  }
   176  
   177  func (r *Reporter) toText() (err error) {
   178  	glog.Infof(fmt.Sprintf("Generating text report,time consuming %vs", time.Since(r.StartTime).Seconds()))
   179  	color.Magenta(
   180  		headerTpl,
   181  		r.Project,
   182  		int(r.GetFinalScore()),
   183  		r.Grade,
   184  		r.TimeStamp,
   185  		r.Issues,
   186  	)
   187  	for _, metric := range r.Metrics {
   188  		if metric.Name == "DependGraph" || 0 == len(metric.Summaries) {
   189  			continue
   190  		}
   191  		color.Cyan(metricsHeaderTpl, metric.Name, metric.Description)
   192  		for _, summary := range metric.Summaries {
   193  			color.Blue(summaryHeaderTpl, summary.Name, summary.Description)
   194  			for _, errorInfo := range summary.Errors {
   195  				color.Red(errorInfoTpl, errorInfo.ErrorString, errorInfo.LineNumber)
   196  			}
   197  		}
   198  	}
   199  	return
   200  }
   201  
   202  // toHtml will rebuild the reporter's json data into html data in template.
   203  // It will parse json data and organize the data structure.
   204  func (r *Reporter) toHtml() (err error) {
   205  	glog.Infof(fmt.Sprintf("Generating json report,time consuming %vs", time.Since(r.StartTime).Seconds()))
   206  	jsonReport, err := jsoniter.Marshal(r)
   207  	if err != nil {
   208  		return
   209  	}
   210  	if jsonReport == nil {
   211  		return errors.New("json is null")
   212  	}
   213  
   214  	var htmlData HtmlData
   215  	issues := 0
   216  
   217  	htmlData.Project = r.Project
   218  	htmlData.Score = int(r.GetFinalScore())
   219  	// convert all linter's data.
   220  	htmlData.converterCodeTest(*r)
   221  	htmlData.converterCodeSmell(*r)
   222  	htmlData.converterCodeOptimization(*r)
   223  	htmlData.converterCodeStyle(*r)
   224  	htmlData.converterCodeCount(*r)
   225  	htmlData.converterDependGraph(*r)
   226  
   227  	noTestPackages := make([]string, 0)
   228  	importPackages := r.Metrics["ImportPackagesTips"].Summaries
   229  	unitTestPackages := r.Metrics["UnitTestTips"].Summaries
   230  	for packageName, _ := range importPackages {
   231  		if _, ok := unitTestPackages[packageName]; !ok {
   232  			noTestPackages = append(noTestPackages, packageName)
   233  		}
   234  	}
   235  	htmlData.IssuesNum = issues
   236  	htmlData.Date = r.TimeStamp
   237  
   238  	SaveAsHtml(htmlData, r.ProjectPath, r.ReportPath, r.TimeStamp, r.HtmlTemplate)
   239  
   240  	return
   241  }
   242  
   243  func (r *Reporter) GetFinalScore() (score float64) {
   244  	for _, metric := range r.Metrics {
   245  		score = score + metric.Percentage*metric.Weight
   246  	}
   247  	return
   248  }
   249  
   250  func NewReporter(projectPath, reportPath, reportFormat, htmlTemplate string) *Reporter {
   251  	return &Reporter{
   252  		StartTime:    time.Now(),
   253  		Metrics:      make(map[string]Metric, 0),
   254  		ProjectPath:  projectPath,
   255  		ReportPath:   reportPath,
   256  		ReportFormat: reportFormat,
   257  		HtmlTemplate: htmlTemplate,
   258  	}
   259  }
   260  
   261  func (r *Reporter) AddLinters(strategies ...StrategyLinter) {
   262  	r.Linters = append(r.Linters, strategies...)
   263  }
   264  
   265  func (r *Reporter) Close() {
   266  	close(r.Sync.LintersFinishedSignal)
   267  	close(r.Sync.LintersProcessChans)
   268  }
   269  
   270  // Error contains the line number and the reason for
   271  // an error output from a command
   272  type Error struct {
   273  	LineNumber  int    `json:"line_number"`
   274  	ErrorString string `json:"error_string"`
   275  }
   276  
   277  // FileSummary contains the filename, location of the file
   278  // on GitHub, and all of the errors related to the file
   279  type Summary struct {
   280  	Name        string  `json:"name"`
   281  	Description string  `json:"description"`
   282  	Errors      []Error `json:"errors"`
   283  	SumCover    float64
   284  	CountCover  int
   285  	Avg         float64
   286  }
   287  
   288  type Summaries struct {
   289  	Summaries map[string]Summary
   290  	sync.RWMutex
   291  }
   292  
   293  func NewSummaries() *Summaries {
   294  	return &Summaries{Summaries: make(map[string]Summary, 0)}
   295  }
   296  
   297  // Metric as template of report and will save all linters result
   298  // data.But may have some difference in different linter.
   299  type Metric struct {
   300  	Name        string             `json:"name"`
   301  	Description string             `json:"description"`
   302  	Summaries   map[string]Summary `json:"summaries"`
   303  	Weight      float64            `json:"weight"`
   304  	Percentage  float64            `json:"percentage"`
   305  	Error       string             `json:"error"`
   306  }