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 }