Blank identifier discarding results; verify intentional ignoring of return values
_ = w.Write(record)
1// SPDX-License-Identifier: MIT23package processor45import (6 "bytes"7 "cmp"8 "encoding/csv"9 "fmt"10 "math"11 "os"12 "path/filepath"13 "regexp"14 "slices"15 "strconv"16 "strings"17 "time"1819 jsoniter "github.com/json-iterator/go"20 "github.com/mattn/go-runewidth"21 "go.yaml.in/yaml/v2"2223 glanguage "golang.org/x/text/language"24 gmessage "golang.org/x/text/message"25)2627var tabularShortBreak = "───────────────────────────────────────────────────────────────────────────────\n"28var tabularShortBreakCi = "-------------------------------------------------------------------------------\n"2930var tabularShortFormatHead = "%-15s %9s %11s %9s %9s %10s %10s\n"31var tabularShortFormatBody = "%-15s %9d %11d %9d %9d %10d %10d\n"32var tabularShortFormatFile = "%s %9d %9d %9d %10d %10d\n"33var tabularShortFormatFileMaxMean = "MaxLine / MeanLine %6d %11d\n"34var shortFormatFileTruncate = 2635var shortNameTruncate = 1536var tabularShortUlocLanguageFormatBody = "(ULOC) %30d\n"37var tabularShortPercentLanguageFormatBody = "Percentage %13.1f%% %10.1f%% %8.1f%% %8.1f%% %9.1f%% %9.1f%%\n"38var tabularShortUlocGlobalFormatBody = "Unique Lines of Code (ULOC) %9d\n"39var tabularShortDrynessFormatBody = "DRYness %% %27.2f\n"4041var tabularShortFormatHeadNoComplexity = "%-21s %11s %11s %10s %11s %10s\n"42var tabularShortFormatBodyNoComplexity = "%-21s %11d %11d %10d %11d %10d\n"43var tabularShortFormatFileNoComplexity = "%s %10d %10d %11d %10d\n"44var tabularShortFormatFileMaxMeanNoComplexity = "MaxLine / MeanLine %14d %11d\n"45var longNameTruncate = 2246var tabularShortUlocLanguageFormatBodyNoComplexity = "(ULOC) %38d\n"47var tabularShortPercentLanguageFormatBodyNoComplexity = "Percentage %21.1f%% %10.1f%% %9.1f%% %10.1f%% %9.1f%%\n"4849var tabularWideBreak = "─────────────────────────────────────────────────────────────────────────────────────────────────────────────\n"50var tabularWideBreakCi = "-------------------------------------------------------------------------------------------------------------\n"51var tabularWideFormatHead = "%-33s %9s %9s %8s %9s %8s %10s %16s\n"52var tabularWideFormatBody = "%-33s %9d %9d %8d %9d %8d %10d %16.2f\n"53var tabularWideFormatFile = "%s %9d %8d %9d %8d %10d %16.2f\n"54var tabularWideFormatFileMaxMean = "MaxLine / MeanLine %24d %9d\n"55var wideFormatFileTruncate = 4256var tabularWideUlocLanguageFormatBody = "(ULOC) %46d\n"57var tabularWideUlocGlobalFormatBody = "Unique Lines of Code (ULOC) %25d\n"58var tabularWideFormatBodyPercent = "Percentage %31.1f%% %8.1f%% %7.1f%% %8.1f%% %7.1f%% %9.1f%%\n"59var tabularWideDrynessFormatBody = "DRYness %% %43.2f\n"6061var openMetricsMetadata = `# TYPE scc_files gauge62# HELP scc_files Number of sourcecode files.63# TYPE scc_lines gauge64# HELP scc_lines Number of lines.65# TYPE scc_code gauge66# HELP scc_code Number of lines of actual code.67# TYPE scc_comments gauge68# HELP scc_comments Number of comments.69# TYPE scc_blanks gauge70# HELP scc_blanks Number of blank lines.71# TYPE scc_complexity gauge72# HELP scc_complexity Code complexity.73# TYPE scc_bytes gauge74# UNIT scc_bytes bytes75# HELP scc_bytes Size in bytes.76`77var openMetricsSummaryRecordFormat = "scc_%s{language=\"%s\"} %d\n"78var openMetricsFileRecordFormat = "scc_%s{language=\"%s\",file=\"%s\"} %d\n"7980func sortSummaryFiles(summary *LanguageSummary) {81 switch SortBy {82 case "name", "names", "language", "languages", "lang", "langs":83 slices.SortFunc(summary.Files, func(a, b *FileJob) int {84 return strings.Compare(a.Location, b.Location)85 })86 case "line", "lines":87 slices.SortFunc(summary.Files, func(a, b *FileJob) int {88 return cmp.Compare(b.Lines, a.Lines)89 })90 case "blank", "blanks":91 slices.SortFunc(summary.Files, func(a, b *FileJob) int {92 return cmp.Compare(b.Blank, a.Blank)93 })94 case "code", "codes":95 slices.SortFunc(summary.Files, func(a, b *FileJob) int {96 return cmp.Compare(b.Code, a.Code)97 })98 case "comment", "comments":99 slices.SortFunc(summary.Files, func(a, b *FileJob) int {100 return cmp.Compare(b.Comment, a.Comment)101 })102 case "complexity", "complexitys", "comp":103 slices.SortFunc(summary.Files, func(a, b *FileJob) int {104 return cmp.Compare(b.Complexity, a.Complexity)105 })106 default:107 slices.SortFunc(summary.Files, func(a, b *FileJob) int {108 return cmp.Compare(b.Lines, a.Lines)109 })110 }111}112113// LanguageSummary to generate output like cloc114type languageSummaryCloc struct {115 Name string `yaml:"name"`116 Code int64 `yaml:"code"`117 Comment int64 `yaml:"comment"`118 Blank int64 `yaml:"blank"`119 Count int64 `yaml:"nFiles"`120}121122type summaryStruct struct {123 Code int64 `yaml:"code"`124 Comment int64 `yaml:"comment"`125 Blank int64 `yaml:"blank"`126 Count int64 `yaml:"nFiles"`127}128129type headerStruct struct {130 Url string `yaml:"url"`131 Version string `yaml:"version"`132 ElapsedSeconds float64 `yaml:"elapsed_seconds"`133 NFiles int64 `yaml:"n_files"`134 NLines int64 `yaml:"n_lines"`135 FilesPerSecond float64 `yaml:"files_per_second"`136 LinesPerSecond float64 `yaml:"lines_per_second"`137}138139type languageReportStart struct {140 Header headerStruct141}142143type languageReportEnd struct {144 Sum summaryStruct `yaml:"SUM"`145}146147func getTabularShortBreak() string {148 if HBorder {149 return ""150 }151152 if Ci {153 return tabularShortBreakCi154 }155156 return tabularShortBreak157}158159func getTabularWideBreak() string {160 if HBorder {161 return ""162 }163164 if Ci {165 return tabularWideBreakCi166 }167168 return tabularWideBreak169}170171func toClocYAML(input chan *FileJob) string {172 startTime := makeTimestampMilli()173174 langs := map[string]languageSummaryCloc{}175 var sumFiles, sumLines, sumCode, sumComment, sumBlank, sumComplexity int64 = 0, 0, 0, 0, 0, 0176177 for res := range input {178 sumFiles++179 sumLines += res.Lines180 sumCode += res.Code181 sumComment += res.Comment182 sumBlank += res.Blank183 sumComplexity += res.Complexity184185 _, ok := langs[res.Language]186187 if !ok {188 langs[res.Language] = languageSummaryCloc{189 Name: res.Language,190 Code: res.Code,191 Comment: res.Comment,192 Blank: res.Blank,193 Count: 1,194 }195 } else {196 tmp := langs[res.Language]197198 langs[res.Language] = languageSummaryCloc{199 Name: res.Language,200 Code: tmp.Code + res.Code,201 Comment: tmp.Comment + res.Comment,202 Blank: tmp.Blank + res.Blank,203 Count: tmp.Count + 1,204 }205 }206 }207208 es := float64(makeTimestampMilli()-startTimeMilli) * float64(0.001)209210 header := headerStruct{211 Url: "https://github.com/boyter/scc/",212 Version: Version,213 NFiles: sumFiles,214 NLines: sumLines,215 ElapsedSeconds: es,216 FilesPerSecond: float64(float64(sumFiles) / es),217 LinesPerSecond: float64(float64(sumLines) / es),218 }219 summary := summaryStruct{220 Blank: sumBlank,221 Comment: sumComment,222 Code: sumCode,223 Count: sumFiles,224 }225 reportStart := languageReportStart{226 Header: header,227 }228 reportEnd := languageReportEnd{229 Sum: summary,230 }231232 reportYaml, _ := yaml.Marshal(reportStart)233 sumYaml, _ := yaml.Marshal(reportEnd)234 languageYaml, _ := yaml.Marshal(langs)235 yamlString := "# https://github.com/boyter/scc/\n" + string(reportYaml) + string(languageYaml) + string(sumYaml)236237 printDebugF("milliseconds to build formatted string: %d", makeTimestampMilli()-startTime)238239 return yamlString240}241242func toJSON(input chan *FileJob) string {243 startTime := makeTimestampMilli()244 language := aggregateLanguageSummary(input)245 language = sortLanguageSummary(language)246247 json := jsoniter.ConfigCompatibleWithStandardLibrary248 jsonString, _ := json.Marshal(language)249250 printDebugF("milliseconds to build formatted string: %d", makeTimestampMilli()-startTime)251252 return string(jsonString)253}254255type Json2 struct {256 LanguageSummary []LanguageSummary `json:"languageSummary"`257 EstimatedCost float64 `json:"estimatedCost"`258 EstimatedScheduleMonths float64 `json:"estimatedScheduleMonths"`259 EstimatedPeople float64 `json:"estimatedPeople"`260261 // LOCOMO fields (only populated when --locomo or --cost-comparison is enabled)262 EstimatedLLMCost *float64 `json:"estimatedLLMCost,omitempty"`263 EstimatedLLMInputTokens *float64 `json:"estimatedLLMInputTokens,omitempty"`264 EstimatedLLMOutputTokens *float64 `json:"estimatedLLMOutputTokens,omitempty"`265 EstimatedLLMGenerationSeconds *float64 `json:"estimatedLLMGenerationSeconds,omitempty"`266 EstimatedLLMReviewHours *float64 `json:"estimatedLLMReviewHours,omitempty"`267 EstimatedLLMPreset *string `json:"estimatedLLMPreset,omitempty"`268 EstimatedLLMAverageComplexityMult *float64 `json:"estimatedLLMAverageComplexityMultiplier,omitempty"`269 EstimatedLLMCycles *float64 `json:"estimatedLLMCycles,omitempty"`270}271272func toJSON2(input chan *FileJob) string {273 startTime := makeTimestampMilli()274 language := aggregateLanguageSummary(input)275 language = sortLanguageSummary(language)276277 var sumCode, sumComplexity int64278 for _, l := range language {279 sumCode += l.Code280 sumComplexity += l.Complexity281 }282283 cost, schedule, people := esstimateCostScheduleMonths(sumCode)284285 j2 := Json2{286 LanguageSummary: language,287 EstimatedCost: cost,288 EstimatedScheduleMonths: schedule,289 EstimatedPeople: people,290 }291292 if Locomo {293 result := LocomoEstimate(sumCode, sumComplexity)294 j2.EstimatedLLMCost = &result.Cost295 j2.EstimatedLLMInputTokens = &result.InputTokens296 j2.EstimatedLLMOutputTokens = &result.OutputTokens297 j2.EstimatedLLMGenerationSeconds = &result.GenerationSeconds298 j2.EstimatedLLMReviewHours = &result.ReviewHours299 j2.EstimatedLLMPreset = &result.Preset300 j2.EstimatedLLMAverageComplexityMult = &result.AverageComplexityMult301 j2.EstimatedLLMCycles = &result.IterationFactor302 }303304 json := jsoniter.ConfigCompatibleWithStandardLibrary305 jsonString, _ := json.Marshal(j2)306307 printDebugF("milliseconds to build formatted string: %d", makeTimestampMilli()-startTime)308309 return string(jsonString)310}311312func toCSV(input chan *FileJob) string {313 if Files {314 return toCSVFiles(input)315 }316317 return toCSVSummary(input)318}319320func toCSVSummary(input chan *FileJob) string {321 language := aggregateLanguageSummary(input)322 language = sortLanguageSummary(language)323324 record := []string{325 "Language",326 "Lines",327 "Code",328 "Comments",329 "Blanks",330 "Complexity",331 "Bytes",332 "Files",333 "ULOC",334 }335336 b := &bytes.Buffer{}337 w := csv.NewWriter(b)338 _ = w.Write(record)339340 for _, result := range language {341 record[0] = result.Name342 record[1] = strconv.FormatInt(result.Lines, 10)343 record[2] = strconv.FormatInt(result.Code, 10)344 record[3] = strconv.FormatInt(result.Comment, 10)345 record[4] = strconv.FormatInt(result.Blank, 10)346 record[5] = strconv.FormatInt(result.Complexity, 10)347 record[6] = strconv.FormatInt(result.Bytes, 10)348 record[7] = strconv.FormatInt(result.Count, 10)349 record[8] = strconv.Itoa(len(ulocLanguageCount[result.Name]))350 _ = w.Write(record)351 }352353 w.Flush()354355 return b.String()356}357358func getCSVFilesSortFunc(sortBy string) func(a, b []string) int {359 // Cater for the common case of adding plural even for those options that don't make sense360 // as it's quite common for those who English is not a first language to make a simple mistake361 switch sortBy {362 case "name", "names":363 return func(a, b []string) int {364 return strings.Compare(a[2], b[2])365 }366 case "language", "languages", "lang", "langs":367 return func(a, b []string) int {368 return strings.Compare(a[0], b[0])369 }370 case "line", "lines":371 return func(a, b []string) int {372 i1, _ := strconv.ParseInt(a[3], 10, 64)373 i2, _ := strconv.ParseInt(b[3], 10, 64)374 return cmp.Compare(i2, i1)375 }376 case "blank", "blanks":377 return func(a, b []string) int {378 i1, _ := strconv.ParseInt(a[6], 10, 64)379 i2, _ := strconv.ParseInt(b[6], 10, 64)380 return cmp.Compare(i2, i1)381 }382 case "code", "codes":383 return func(a, b []string) int {384 i1, _ := strconv.ParseInt(a[4], 10, 64)385 i2, _ := strconv.ParseInt(b[4], 10, 64)386 return cmp.Compare(i2, i1)387 }388 case "comment", "comments":389 return func(a, b []string) int {390 i1, _ := strconv.ParseInt(a[5], 10, 64)391 i2, _ := strconv.ParseInt(b[5], 10, 64)392 return cmp.Compare(i2, i1)393 }394 case "complexity", "complexitys":395 return func(a, b []string) int {396 i1, _ := strconv.ParseInt(a[7], 10, 64)397 i2, _ := strconv.ParseInt(b[7], 10, 64)398 return cmp.Compare(i2, i1)399 }400 case "byte", "bytes":401 return func(a, b []string) int {402 i1, _ := strconv.ParseInt(a[8], 10, 64)403 i2, _ := strconv.ParseInt(b[8], 10, 64)404 return cmp.Compare(i2, i1)405 }406 default:407 return func(a, b []string) int {408 return strings.Compare(a[2], b[2])409 }410 }411}412413func toCSVFiles(input chan *FileJob) string {414 records := [][]string{}415416 for result := range input {417 records = append(records, []string{418 result.Language,419 result.Location,420 result.Filename,421 strconv.FormatInt(result.Lines, 10),422 strconv.FormatInt(result.Code, 10),423 strconv.FormatInt(result.Comment, 10),424 strconv.FormatInt(result.Blank, 10),425 strconv.FormatInt(result.Complexity, 10),426 strconv.FormatInt(result.Bytes, 10),427 strconv.Itoa(result.Uloc),428 })429 }430431 slices.SortFunc(records, getCSVFilesSortFunc(SortBy))432433 recordsEnd := [][]string{{434 "Language",435 "Provider",436 "Filename",437 "Lines",438 "Code",439 "Comments",440 "Blanks",441 "Complexity",442 "Bytes",443 "ULOC",444 }}445446 recordsEnd = append(recordsEnd, records...)447448 b := &bytes.Buffer{}449 w := csv.NewWriter(b)450 _ = w.WriteAll(recordsEnd)451 w.Flush()452453 return b.String()454}455456func toOpenMetrics(input chan *FileJob) string {457 if Files {458 return toOpenMetricsFiles(input)459 }460461 return toOpenMetricsSummary(input)462}463464func toOpenMetricsSummary(input chan *FileJob) string {465 language := aggregateLanguageSummary(input)466 language = sortLanguageSummary(language)467468 sb := &strings.Builder{}469 sb.WriteString(openMetricsMetadata)470 for _, result := range language {471 _, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "files", result.Name, result.Count)472 _, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "lines", result.Name, result.Lines)473 _, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "code", result.Name, result.Code)474 _, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "comments", result.Name, result.Comment)475 _, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "blanks", result.Name, result.Blank)476 _, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "complexity", result.Name, result.Complexity)477 _, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "bytes", result.Name, result.Bytes)478 }479 return sb.String()480}481482func toOpenMetricsFiles(input chan *FileJob) string {483 sb := &strings.Builder{}484 sb.WriteString(openMetricsMetadata)485 for file := range input {486 var filename = strings.ReplaceAll(file.Location, "\\", "\\\\")487 _, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "lines", file.Language, filename, file.Lines)488 _, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "code", file.Language, filename, file.Code)489 _, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "comments", file.Language, filename, file.Comment)490 _, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "blanks", file.Language, filename, file.Blank)491 _, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "complexity", file.Language, filename, file.Complexity)492 _, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "bytes", file.Language, filename, file.Bytes)493 }494 sb.WriteString("# EOF\n")495 return sb.String()496}497498// For very large repositories CSV stream can be used which prints results out as they come in499// with the express idea of lowering memory usage, see https://github.com/boyter/scc/issues/210 for500// the background on why this might be needed501func toCSVStream(input chan *FileJob) string {502 fmt.Println("Language,Provider,Filename,Lines,Code,Comments,Blanks,Complexity,Bytes,Uloc")503504 var quoteRegex = regexp.MustCompile("\"")505506 for result := range input {507 // Escape quotes in location and filename then surround with quotes.508 var location = "\"" + quoteRegex.ReplaceAllString(result.Location, "\"\"") + "\""509 var filename = "\"" + quoteRegex.ReplaceAllString(result.Filename, "\"\"") + "\""510511 fmt.Printf("%s,%s,%s,%d,%d,%d,%d,%d,%d,%d\n",512 result.Language,513 location,514 filename,515 result.Lines,516 result.Code,517 result.Comment,518 result.Blank,519 result.Complexity,520 result.Bytes,521 result.Uloc,522 )523 }524525 return ""526}527528func toHtml(input chan *FileJob) string {529 return `<html lang="en"><head><meta charset="utf-8" /><title>scc html output</title><style>table { border-collapse: collapse; }td, th { border: 1px solid #999; padding: 0.5rem; text-align: left;}</style></head><body>` +530 toHtmlTable(input) +531 "</body></html>\n"532}533534func toHtmlTable(input chan *FileJob) string {535 languages := map[string]LanguageSummary{}536 var sumFiles, sumLines, sumCode, sumComment, sumBlank, sumComplexity, sumBytes int64 = 0, 0, 0, 0, 0, 0, 0537538 for res := range input {539 sumFiles++540 sumLines += res.Lines541 sumCode += res.Code542 sumComment += res.Comment543 sumBlank += res.Blank544 sumComplexity += res.Complexity545 sumBytes += res.Bytes546547 _, ok := languages[res.Language]548549 if !ok {550 files := []*FileJob{}551 files = append(files, res)552553 languages[res.Language] = LanguageSummary{554 Name: res.Language,555 Lines: res.Lines,556 Code: res.Code,557 Comment: res.Comment,558 Blank: res.Blank,559 Complexity: res.Complexity,560 Count: 1,561 Files: files,562 Bytes: res.Bytes,563 }564 } else {565 tmp := languages[res.Language]566 files := append(tmp.Files, res)567568 languages[res.Language] = LanguageSummary{569 Name: res.Language,570 Lines: tmp.Lines + res.Lines,571 Code: tmp.Code + res.Code,572 Comment: tmp.Comment + res.Comment,573 Blank: tmp.Blank + res.Blank,574 Complexity: tmp.Complexity + res.Complexity,575 Count: tmp.Count + 1,576 Files: files,577 Bytes: tmp.Bytes + res.Bytes,578 }579 }580 }581582 language := make([]LanguageSummary, 0, len(languages))583 for _, summary := range languages {584 language = append(language, summary)585 }586587 language = sortLanguageSummary(language)588589 str := &strings.Builder{}590591 str.WriteString(`<table id="scc-table">592 <thead><tr>593 <th>Language</th>594 <th>Files</th>595 <th>Lines</th>596 <th>Blank</th>597 <th>Comment</th>598 <th>Code</th>599 <th>Complexity</th>600 <th>Bytes</th>601 <th>Uloc</th>602 </tr></thead>603 <tbody>`)604605 for _, r := range language {606 _, _ = fmt.Fprintf(str, `<tr>607 <th>%s</th>608 <th>%d</th>609 <th>%d</th>610 <th>%d</th>611 <th>%d</th>612 <th>%d</th>613 <th>%d</th>614 <th>%d</th>615 <th>%d</th>616 </tr>`, r.Name, len(r.Files), r.Lines, r.Blank, r.Comment, r.Code, r.Complexity, r.Bytes, len(ulocLanguageCount[r.Name]))617618 if Files {619 sortSummaryFiles(&r)620621 for _, res := range r.Files {622 _, _ = fmt.Fprintf(str, `<tr>623 <td>%s</td>624 <td></td>625 <td>%d</td>626 <td>%d</td>627 <td>%d</td>628 <td>%d</td>629 <td>%d</td>630 <td>%d</td>631 <td>%d</td>632 </tr>`, res.Location, res.Lines, res.Blank, res.Comment, res.Code, res.Complexity, res.Bytes, res.Uloc)633 }634 }635636 }637638 _, _ = fmt.Fprintf(str, `</tbody>639 <tfoot><tr>640 <th>Total</th>641 <th>%d</th>642 <th>%d</th>643 <th>%d</th>644 <th>%d</th>645 <th>%d</th>646 <th>%d</th>647 <th>%d</th>648 <th>%d</th>649 </tr>`, sumFiles, sumLines, sumBlank, sumComment, sumCode, sumComplexity, sumBytes, len(ulocGlobalCount))650651 hasCostOutput := false652 if !Cocomo {653 var sb strings.Builder654 calculateCocomo(sumCode, &sb)655 _, _ = fmt.Fprintf(str, `656 <tr>657 <th colspan="9">%s</th>658 </tr>`, strings.ReplaceAll(sb.String(), "\n", "<br>"))659 hasCostOutput = true660 }661 if Locomo {662 var sb strings.Builder663 calculateLocomo(sumCode, sumComplexity, &sb)664 _, _ = fmt.Fprintf(str, `665 <tr>666 <th colspan="9">%s</th>667 </tr>`, strings.ReplaceAll(sb.String(), "\n", "<br>"))668 hasCostOutput = true669 }670 if hasCostOutput {671 str.WriteString(`</tfoot>672 </table>`)673 } else {674 str.WriteString(`</tfoot></table>`)675 }676677 return str.String()678}679680func toSqlInsert(input chan *FileJob) string {681 str := &strings.Builder{}682 projectName := SQLProject683 if projectName == "" {684 projectName = strings.Join(DirFilePaths, ",")685 }686687 var sumCode, sumComplexity int64688 str.WriteString("\nbegin transaction;")689 count := 0690 for res := range input {691 count++692 sumCode += res.Code693 sumComplexity += res.Complexity694695 dir, _ := filepath.Split(res.Location)696697 _, _ = fmt.Fprintf(str, "\ninsert into t values('%s', '%s', '%s', '%s', '%s', %d, %d, %d, %d, %d, %d);",698 escapeSQLString(projectName),699 escapeSQLString(res.Language),700 escapeSQLString(res.Location),701 escapeSQLString(dir),702 escapeSQLString(res.Filename), res.Bytes, res.Blank, res.Comment, res.Code, res.Complexity, res.Uloc)703704 // every 1000 files commit and start a new transaction to avoid overloading705 if count == 1000 {706 str.WriteString("\ncommit;")707 str.WriteString("\nbegin transaction;")708 count = 0709 }710 }711 str.WriteString("\ncommit;")712713 cost, schedule, people := esstimateCostScheduleMonths(sumCode)714 currentTime := time.Now()715 es := float64(makeTimestampMilli()-startTimeMilli) * 0.001716 str.WriteString("\nbegin transaction;")717 _, _ = fmt.Fprintf(str, "\ninsert into metadata values('%s', '%s', %f, %f, %f, %f);",718 currentTime.Format("2006-01-02 15:04:05"),719 projectName,720 es,721 cost,722 schedule,723 people,724 )725 str.WriteString("\ncommit;")726727 if Locomo {728 result := LocomoEstimate(sumCode, sumComplexity)729 str.WriteString("\nbegin transaction;")730 _, _ = fmt.Fprintf(str, "\ninsert into locomo_metadata values('%s', '%s', %f, %f, %f, %f, %f, '%s', %f);",731 currentTime.Format("2006-01-02 15:04:05"),732 projectName,733 result.Cost,734 result.InputTokens,735 result.OutputTokens,736 result.GenerationSeconds,737 result.ReviewHours,738 escapeSQLString(result.Preset),739 result.IterationFactor,740 )741 str.WriteString("\ncommit;")742 }743744 return str.String()745}746747// attempt to manually escape everything that could be a problem748func escapeSQLString(input string) string {749 var buffer bytes.Buffer750 for _, char := range input {751 switch char {752 case '\x00':753 // Remove null characters754 continue755 case '\'':756 // Escape single quote with another single quote757 buffer.WriteRune('\'')758 buffer.WriteRune('\'')759 default:760 buffer.WriteRune(char)761 }762 }763 return buffer.String()764}765766func toSql(input chan *FileJob) string {767 var str strings.Builder768769 str.WriteString(`create table metadata ( -- github.com/boyter/scc v ` + Version + `770 timestamp text,771 Project text,772 elapsed_s real,773 estimated_cost real,774 estimated_schedule_months real,775 estimated_people real);776create table t (777 Project text ,778 Language text ,779 File text ,780 File_dirname text ,781 File_basename text ,782 nByte integer,783 nBlank integer,784 nComment integer,785 nCode integer,786 nComplexity integer,787 nUloc integer 788);`)789790 str.WriteString(toSqlInsert(input))791 return str.String()792}793794func fileSummarize(input chan *FileJob) string {795 if FormatMulti != "" {796 return fileSummarizeMulti(input)797 }798799 switch {800 case More || strings.EqualFold(Format, "wide"):801 return fileSummarizeLong(input)802 case strings.EqualFold(Format, "json"):803 return toJSON(input)804 case strings.EqualFold(Format, "json2"):805 return toJSON2(input)806 case strings.EqualFold(Format, "cloc-yaml") || strings.EqualFold(Format, "cloc-yml"):807 return toClocYAML(input)808 case strings.EqualFold(Format, "csv"):809 return toCSV(input)810 case strings.EqualFold(Format, "csv-stream"):811 return toCSVStream(input)812 case strings.EqualFold(Format, "html"):813 return toHtml(input)814 case strings.EqualFold(Format, "html-table"):815 return toHtmlTable(input)816 case strings.EqualFold(Format, "sql"):817 return toSql(input)818 case strings.EqualFold(Format, "sql-insert"):819 return toSqlInsert(input)820 case strings.EqualFold(Format, "openmetrics"):821 return toOpenMetrics(input)822 }823824 return fileSummarizeShort(input)825}826827// Deals with the case of CI/CD where you might want to run with multiple outputs828// both to files and to stdout. Not the most efficient way to do it in terms of memory829// but seeing as the files are just summaries by this point it shouldn't be too bad830func fileSummarizeMulti(input chan *FileJob) string {831 // collect all the results832 var results []*FileJob833 for res := range input {834 results = append(results, res)835 }836837 var str strings.Builder838839 // for each output pump the results into840 for s := range strings.SplitSeq(FormatMulti, ",") {841 t := strings.Split(s, ":")842 if len(t) == 2 {843 i := make(chan *FileJob, len(results))844845 for _, r := range results {846 i <- r847 }848 close(i)849850 var val string851852 switch strings.ToLower(t[0]) {853 case "tabular":854 val = fileSummarizeShort(i)855 case "wide":856 val = fileSummarizeLong(i)857 case "json":858 val = toJSON(i)859 case "json2":860 val = toJSON2(i)861 case "cloc-yaml":862 val = toClocYAML(i)863 case "cloc-yml":864 val = toClocYAML(i)865 case "csv":866 val = toCSV(i)867 case "csv-stream":868 // special case where we want to ignore writing to stdout to disk as it's already done869 _ = toCSVStream(i)870 continue871 case "html":872 val = toHtml(i)873 case "html-table":874 val = toHtmlTable(i)875 case "sql":876 val = toSql(i)877 case "sql-insert":878 val = toSqlInsert(i)879 case "openmetrics":880 val = toOpenMetrics(i)881 }882883 if t[1] == "stdout" {884 str.WriteString(val)885 str.WriteString("\n")886 } else {887 err := os.WriteFile(t[1], []byte(val), 0600)888 if err != nil {889 fmt.Printf("%s unable to be written to for format %s: %s", t[1], t[0], err)890 }891 }892 }893 }894895 return str.String()896}897898func fileSummarizeLong(input chan *FileJob) string {899 str := &strings.Builder{}900901 str.WriteString(getTabularWideBreak())902 _, _ = fmt.Fprintf(str, tabularWideFormatHead, "Language", "Files", "Lines", "Blanks", "Comments", "Code", "Complexity", "Complexity/Lines")903904 if !Files {905 str.WriteString(getTabularWideBreak())906 }907908 langs := map[string]LanguageSummary{}909 var sumFiles, sumLines, sumCode, sumComment, sumBlank, sumComplexity, sumBytes int64 = 0, 0, 0, 0, 0, 0, 0910 var sumWeightedComplexity float64911912 for res := range input {913 sumFiles++914 sumLines += res.Lines915 sumCode += res.Code916 sumComment += res.Comment917 sumBlank += res.Blank918 sumComplexity += res.Complexity919 sumBytes += res.Bytes920921 var weightedComplexity float64922 if res.Code != 0 {923 weightedComplexity = (float64(res.Complexity) / float64(res.Code)) * 100924 }925 res.WeightedComplexity = weightedComplexity926 sumWeightedComplexity += weightedComplexity927928 _, ok := langs[res.Language]929930 if !ok {931 files := []*FileJob{}932 files = append(files, res)933934 langs[res.Language] = LanguageSummary{935 Name: res.Language,936 Lines: res.Lines,937 Code: res.Code,938 Comment: res.Comment,939 Blank: res.Blank,940 Complexity: res.Complexity,941 Count: 1,942 WeightedComplexity: weightedComplexity,943 Files: files,944 LineLength: res.LineLength,945 }946 } else {947 tmp := langs[res.Language]948 files := append(tmp.Files, res)949 lineLength := append(tmp.LineLength, res.LineLength...)950951 langs[res.Language] = LanguageSummary{952 Name: res.Language,953 Lines: tmp.Lines + res.Lines,954 Code: tmp.Code + res.Code,955 Comment: tmp.Comment + res.Comment,956 Blank: tmp.Blank + res.Blank,957 Complexity: tmp.Complexity + res.Complexity,958 Count: tmp.Count + 1,959 WeightedComplexity: tmp.WeightedComplexity + weightedComplexity,960 Files: files,961 LineLength: lineLength,962 }963 }964 }965966 language := make([]LanguageSummary, 0, len(langs))967 for _, summary := range langs {968 language = append(language, summary)969 }970971 language = sortLanguageSummary(language)972973 startTime := makeTimestampMilli()974 for _, summary := range language {975 if Files {976 str.WriteString(getTabularWideBreak())977 }978979 trimmedName := summary.Name980 if len(summary.Name) > longNameTruncate {981 trimmedName = summary.Name[:longNameTruncate-1] + "…"982 }983984 _, _ = fmt.Fprintf(str, tabularWideFormatBody, trimmedName, summary.Count, summary.Lines, summary.Blank, summary.Comment, summary.Code, summary.Complexity, summary.WeightedComplexity)985986 if Percent {987 _, _ = fmt.Fprintf(str,988 tabularWideFormatBodyPercent,989 float64(len(summary.Files))/float64(sumFiles)*100,990 float64(summary.Lines)/float64(sumLines)*100,991 float64(summary.Blank)/float64(sumBlank)*100,992 float64(summary.Comment)/float64(sumComment)*100,993 float64(summary.Code)/float64(sumCode)*100,994 float64(summary.Complexity)/float64(sumComplexity)*100,995 )996997 if !UlocMode {998 if !Files && summary.Name != language[len(language)-1].Name {999 str.WriteString(tabularWideBreakCi)1000 }1001 }1002 }10031004 if MaxMean {1005 _, _ = fmt.Fprintf(str, tabularWideFormatFileMaxMean, maxIn(summary.LineLength), meanIn(summary.LineLength))1006 }10071008 if UlocMode {1009 _, _ = fmt.Fprintf(str, tabularWideUlocLanguageFormatBody, len(ulocLanguageCount[summary.Name]))1010 if !Files && summary.Name != language[len(language)-1].Name {1011 str.WriteString(tabularWideBreakCi)1012 }1013 }10141015 if Files {1016 sortSummaryFiles(&summary)1017 str.WriteString(getTabularWideBreak())10181019 for _, res := range summary.Files {1020 tmp := unicodeAwareTrim(res.Location, wideFormatFileTruncate)1021 tmp = unicodeAwareRightPad(tmp, 43)10221023 _, _ = fmt.Fprintf(str, tabularWideFormatFile, tmp, res.Lines, res.Blank, res.Comment, res.Code, res.Complexity, res.WeightedComplexity)1024 }1025 }1026 }10271028 printDebugF("milliseconds to build formatted string: %d", makeTimestampMilli()-startTime)10291030 str.WriteString(getTabularWideBreak())1031 _, _ = fmt.Fprintf(str, tabularWideFormatBody, "Total", sumFiles, sumLines, sumBlank, sumComment, sumCode, sumComplexity, sumWeightedComplexity)1032 str.WriteString(getTabularWideBreak())10331034 if UlocMode {1035 _, _ = fmt.Fprintf(str, tabularWideUlocGlobalFormatBody, len(ulocGlobalCount))1036 if Dryness {1037 dryness := float64(len(ulocGlobalCount)) / float64(sumLines)1038 _, _ = fmt.Fprintf(str, tabularWideDrynessFormatBody, dryness)1039 }1040 str.WriteString(getTabularWideBreak())1041 }10421043 if !Cocomo {1044 if SLOCCountFormat {1045 calculateCocomoSLOCCount(sumCode, str)1046 } else {1047 calculateCocomo(sumCode, str)1048 }1049 }1050 if Locomo {1051 calculateLocomo(sumCode, sumComplexity, str)1052 }1053 if !Size {1054 calculateSize(sumBytes, str)1055 str.WriteString(getTabularWideBreak())1056 }1057 return str.String()1058}10591060// We need to trim the file display for tabular output formats which this does in a unicode aware way1061// to avoid cutting bytes... note that it needs to be expanded to deal with longer display characters at some1062// point in the future1063func unicodeAwareTrim(tmp string, size int) string {1064 // iterate all the runes so we can cut off correctly and get the correct length1065 r := []rune(tmp)10661067 if len(r) > size {1068 for runewidth.StringWidth(tmp) > size {1069 // remove character one at a time till we get the length we want1070 r = r[1:]1071 tmp = string(r)1072 }10731074 tmp = "~" + strings.TrimSpace(tmp)1075 }10761077 return tmp1078}10791080// Using %-30s in string format does not appear to be unicode aware with characters such as1081// 文中 meaning the size is off... which is annoying, so we implement this ourselves to get it1082// right1083func unicodeAwareRightPad(tmp string, size int) string {1084 return runewidth.FillRight(tmp, size)1085}10861087func fileSummarizeShort(input chan *FileJob) string {1088 str := &strings.Builder{}10891090 str.WriteString(getTabularShortBreak())1091 if !Complexity {1092 _, _ = fmt.Fprintf(str, tabularShortFormatHead, "Language", "Files", "Lines", "Blanks", "Comments", "Code", "Complexity")1093 } else {1094 _, _ = fmt.Fprintf(str, tabularShortFormatHeadNoComplexity, "Language", "Files", "Lines", "Blanks", "Comments", "Code")1095 }10961097 if !Files {1098 str.WriteString(getTabularShortBreak())1099 }11001101 lang := map[string]LanguageSummary{}1102 var sumFiles, sumLines, sumCode, sumComment, sumBlank, sumComplexity, sumBytes int64 = 0, 0, 0, 0, 0, 0, 011031104 p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))11051106 for res := range input {1107 sumFiles++1108 sumLines += res.Lines1109 sumCode += res.Code1110 sumComment += res.Comment1111 sumBlank += res.Blank1112 sumComplexity += res.Complexity1113 sumBytes += res.Bytes11141115 _, ok := lang[res.Language]11161117 if !ok {1118 files := []*FileJob{}1119 files = append(files, res)11201121 lang[res.Language] = LanguageSummary{1122 Name: res.Language,1123 Lines: res.Lines,1124 Code: res.Code,1125 Comment: res.Comment,1126 Blank: res.Blank,1127 Complexity: res.Complexity,1128 Count: 1,1129 Files: files,1130 LineLength: res.LineLength,1131 }1132 } else {1133 tmp := lang[res.Language]1134 files := append(tmp.Files, res)1135 lineLength := append(tmp.LineLength, res.LineLength...)11361137 lang[res.Language] = LanguageSummary{1138 Name: res.Language,1139 Lines: tmp.Lines + res.Lines,1140 Code: tmp.Code + res.Code,1141 Comment: tmp.Comment + res.Comment,1142 Blank: tmp.Blank + res.Blank,1143 Complexity: tmp.Complexity + res.Complexity,1144 Count: tmp.Count + 1,1145 Files: files,1146 LineLength: lineLength,1147 }1148 }1149 }11501151 language := make([]LanguageSummary, 0, len(lang))1152 for _, summary := range lang {1153 language = append(language, summary)1154 }11551156 language = sortLanguageSummary(language)11571158 startTime := makeTimestampMilli()1159 for _, summary := range language {1160 addBreak := false1161 if Files {1162 str.WriteString(getTabularShortBreak())1163 }11641165 trimmedName := summary.Name1166 trimmedName = trimNameShort(summary, trimmedName)11671168 if !Complexity {1169 _, _ = p.Fprintf(str, tabularShortFormatBody, trimmedName, summary.Count, summary.Lines, summary.Blank, summary.Comment, summary.Code, summary.Complexity)1170 } else {1171 _, _ = p.Fprintf(str, tabularShortFormatBodyNoComplexity, trimmedName, summary.Count, summary.Lines, summary.Blank, summary.Comment, summary.Code)1172 }11731174 if Percent {1175 if !Complexity {1176 _, _ = p.Fprintf(str,1177 tabularShortPercentLanguageFormatBody,1178 float64(len(summary.Files))/float64(sumFiles)*100,1179 float64(summary.Lines)/float64(sumLines)*100,1180 float64(summary.Blank)/float64(sumBlank)*100,1181 float64(summary.Comment)/float64(sumComment)*100,1182 float64(summary.Code)/float64(sumCode)*100,1183 float64(summary.Complexity)/float64(sumComplexity)*100,1184 )1185 } else {1186 _, _ = p.Fprintf(str,1187 tabularShortPercentLanguageFormatBodyNoComplexity,1188 float64(len(summary.Files))/float64(sumFiles)*100,1189 float64(summary.Lines)/float64(sumLines)*100,1190 float64(summary.Blank)/float64(sumBlank)*100,1191 float64(summary.Comment)/float64(sumComment)*100,1192 float64(summary.Code)/float64(sumCode)*100,1193 )1194 }11951196 addBreak = true1197 }11981199 if MaxMean {1200 if !Complexity {1201 _, _ = p.Fprintf(str, tabularShortFormatFileMaxMean, maxIn(summary.LineLength), meanIn(summary.LineLength))1202 } else {1203 _, _ = p.Fprintf(str, tabularShortFormatFileMaxMeanNoComplexity, maxIn(summary.LineLength), meanIn(summary.LineLength))1204 }12051206 addBreak = true1207 }12081209 if Files {1210 sortSummaryFiles(&summary)1211 str.WriteString(getTabularShortBreak())12121213 for _, res := range summary.Files {1214 tmp := unicodeAwareTrim(res.Location, shortFormatFileTruncate)12151216 if !Complexity {1217 tmp = unicodeAwareRightPad(tmp, 27)1218 _, _ = p.Fprintf(str, tabularShortFormatFile, tmp, res.Lines, res.Blank, res.Comment, res.Code, res.Complexity)1219 } else {1220 tmp = unicodeAwareRightPad(tmp, 34)1221 _, _ = p.Fprintf(str, tabularShortFormatFileNoComplexity, tmp, res.Lines, res.Blank, res.Comment, res.Code)1222 }1223 }1224 }12251226 if UlocMode {1227 if !Complexity {1228 _, _ = p.Fprintf(str, tabularShortUlocLanguageFormatBody, len(ulocLanguageCount[summary.Name]))1229 } else {1230 _, _ = p.Fprintf(str, tabularShortUlocLanguageFormatBodyNoComplexity, len(ulocLanguageCount[summary.Name]))1231 }12321233 addBreak = true1234 }12351236 if addBreak {1237 if !Files && summary.Name != language[len(language)-1].Name {1238 str.WriteString(tabularShortBreakCi)1239 }1240 }1241 }12421243 printDebugF("milliseconds to build formatted string: %d", makeTimestampMilli()-startTime)12441245 str.WriteString(getTabularShortBreak())1246 if !Complexity {1247 _, _ = p.Fprintf(str, tabularShortFormatBody, "Total", sumFiles, sumLines, sumBlank, sumComment, sumCode, sumComplexity)1248 } else {1249 _, _ = p.Fprintf(str, tabularShortFormatBodyNoComplexity, "Total", sumFiles, sumLines, sumBlank, sumComment, sumCode)1250 }1251 str.WriteString(getTabularShortBreak())12521253 if UlocMode {1254 _, _ = p.Fprintf(str, tabularShortUlocGlobalFormatBody, len(ulocGlobalCount))1255 if Dryness {1256 dryness := float64(len(ulocGlobalCount)) / float64(sumLines)1257 _, _ = p.Fprintf(str, tabularShortDrynessFormatBody, dryness)1258 }1259 str.WriteString(getTabularShortBreak())1260 }12611262 if !Cocomo {1263 if SLOCCountFormat {1264 calculateCocomoSLOCCount(sumCode, str)1265 } else {1266 calculateCocomo(sumCode, str)1267 }1268 str.WriteString(getTabularShortBreak())1269 }1270 if Locomo {1271 calculateLocomo(sumCode, sumComplexity, str)1272 str.WriteString(getTabularShortBreak())1273 }1274 if !Size {1275 calculateSize(sumBytes, str)1276 str.WriteString(getTabularShortBreak())1277 }1278 return str.String()1279}12801281func maxIn(i []int) int {1282 if len(i) == 0 {1283 return 01284 }12851286 return slices.Max(i)1287}12881289func meanIn(i []int) int {1290 if len(i) == 0 {1291 return 01292 }12931294 sum := 01295 for _, x := range i {1296 sum += x1297 }12981299 return sum / len(i)1300}13011302func trimNameShort(summary LanguageSummary, trimmedName string) string {1303 if len(summary.Name) > shortNameTruncate {1304 trimmedName = summary.Name[:shortNameTruncate-1] + "…"1305 }1306 return trimmedName1307}13081309func calculateCocomoSLOCCount(sumCode int64, str *strings.Builder) {1310 estimatedEffort := EstimateEffort(int64(sumCode), EAF)1311 estimatedScheduleMonths := EstimateScheduleMonths(estimatedEffort)1312 estimatedPeopleRequired := 0.01313 if estimatedScheduleMonths > 0 {1314 estimatedPeopleRequired = estimatedEffort / estimatedScheduleMonths1315 }1316 estimatedCost := EstimateCost(estimatedEffort, AverageWage, Overhead)13171318 p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))13191320 _, _ = p.Fprintf(str, "Total Physical Source Lines of Code (SLOC) = %d\n", sumCode)1321 _, _ = p.Fprintf(str, "Development Effort Estimate, Person-Years (Person-Months) = %.2f (%.2f)\n", estimatedEffort/12, estimatedEffort)1322 _, _ = p.Fprintf(str, " (Basic COCOMO model, Person-Months = %.2f*(KSLOC**%.2f)*%.2f)\n", projectType[CocomoProjectType][0], projectType[CocomoProjectType][1], EAF)1323 _, _ = p.Fprintf(str, "Schedule Estimate, Years (Months) = %.2f (%.2f)\n", estimatedScheduleMonths/12, estimatedScheduleMonths)1324 _, _ = p.Fprintf(str, " (Basic COCOMO model, Months = %.2f*(person-months**%.2f))\n", projectType[CocomoProjectType][2], projectType[CocomoProjectType][3])1325 _, _ = p.Fprintf(str, "Estimated Average Number of Developers (Effort/Schedule) = %.2f\n", estimatedPeopleRequired)1326 _, _ = p.Fprintf(str, "Total Estimated Cost to Develop = %s%.0f\n", CurrencySymbol, estimatedCost)1327 _, _ = p.Fprintf(str, " (average salary = %s%d/year, overhead = %.2f)\n", CurrencySymbol, AverageWage, Overhead)1328}13291330func calculateCocomo(sumCode int64, str *strings.Builder) {1331 estimatedCost, estimatedScheduleMonths, estimatedPeopleRequired := esstimateCostScheduleMonths(sumCode)13321333 p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))13341335 _, _ = p.Fprintf(str, "Estimated Cost to Develop (%s) %s%d\n", CocomoProjectType, CurrencySymbol, int64(estimatedCost))1336 _, _ = p.Fprintf(str, "Estimated Schedule Effort (%s) %.2f months\n", CocomoProjectType, estimatedScheduleMonths)1337 if math.IsNaN(estimatedPeopleRequired) {1338 _, _ = p.Fprintf(str, "Estimated People Required 1 Grandparent\n")1339 } else {1340 _, _ = p.Fprintf(str, "Estimated People Required (%s) %.2f\n", CocomoProjectType, estimatedPeopleRequired)1341 }1342}13431344func esstimateCostScheduleMonths(sumCode int64) (float64, float64, float64) {1345 estimatedEffort := EstimateEffort(int64(sumCode), EAF)1346 estimatedCost := EstimateCost(estimatedEffort, AverageWage, Overhead)1347 estimatedScheduleMonths := EstimateScheduleMonths(estimatedEffort)1348 estimatedPeopleRequired := 0.01349 if estimatedScheduleMonths > 0 {1350 estimatedPeopleRequired = estimatedEffort / estimatedScheduleMonths1351 }1352 return estimatedCost, estimatedScheduleMonths, estimatedPeopleRequired1353}13541355func calculateLocomo(sumCode, sumComplexity int64, str *strings.Builder) {1356 result := LocomoEstimate(sumCode, sumComplexity)13571358 p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))13591360 _, _ = p.Fprintf(str, "LOCOMO LLM Cost Estimate (%s)\n", result.Preset)1361 _, _ = p.Fprintf(str, " Tokens Required (in/out) %.1fM / %.1fM\n", result.InputTokens/1_000_000, result.OutputTokens/1_000_000)1362 _, _ = p.Fprintf(str, " Cost to Generate %s%.0f\n", CurrencySymbol, result.Cost)1363 _, _ = p.Fprintf(str, " Estimated Cycles %.1f\n", result.IterationFactor)13641365 if result.GenerationSeconds > 86400 {1366 _, _ = p.Fprintf(str, " Generation Time (serial) %.1f days\n", result.GenerationSeconds/86400)1367 } else if result.GenerationSeconds > 3600 {1368 _, _ = p.Fprintf(str, " Generation Time (serial) %.1f hours\n", result.GenerationSeconds/3600)1369 } else {1370 _, _ = p.Fprintf(str, " Generation Time (serial) %.1f minutes\n", result.GenerationSeconds/60)1371 }13721373 _, _ = p.Fprintf(str, " Human Review Time %.1f hours\n", result.ReviewHours)1374 str.WriteString(" Disclaimer: rough ballpark for regenerating code using a LLM.\n")1375 str.WriteString(" Does not account for context reuse, test generation, or heavy debugging.\n")1376}13771378func calculateSize(sumBytes int64, str *strings.Builder) {13791380 var size float6413811382 switch strings.ToLower(SizeUnit) {1383 case "binary":1384 size = float64(sumBytes) / 1_048_5761385 case "mixed":1386 size = float64(sumBytes) / 1_024_0001387 case "xkcd-kb":1388 str.WriteString("1000 bytes during leap years, 1024 otherwise\n")1389 if isLeapYear(time.Now().Year()) {1390 size = float64(sumBytes) / 1_000_0001391 }1392 case "xkcd-kelly":1393 str.WriteString("compromise between 1000 and 1024 bytes\n")1394 size = float64(sumBytes) / (1012 * 1012)1395 case "xkcd-imaginary":1396 str.WriteString("used in quantum computing\n")1397 _, _ = fmt.Fprintf(str, "Processed %d bytes, %s megabytes (%s)\n", sumBytes, `¯\_(ツ)_/¯`, strings.ToUpper(SizeUnit))1398 case "xkcd-intel":1399 str.WriteString("calculated on pentium F.P.U.\n")1400 size = float64(sumBytes) / (1023.937528 * 1023.937528)1401 case "xkcd-drive":1402 str.WriteString("shrinks by 4 bytes every year for marketing reasons\n")1403 tim := time.Now()14041405 s := 908 - ((tim.Year() - 2013) * 4) // comic starts with 908 in 2013 hence hardcoded values1406 s = min(s, 908) // just in case the clock is stupidly set14071408 size = float64(sumBytes) / float64(s*s)1409 case "xkcd-bakers":1410 str.WriteString("9 bits to the byte since you're such a good customer\n")1411 size = float64(sumBytes) / (1152 * 1152)1412 default:1413 // SI value of 1000 bytes1414 size = float64(sumBytes) / 1_000_0001415 SizeUnit = "SI"1416 }14171418 if !strings.EqualFold(SizeUnit, "xkcd-imaginary") {1419 _, _ = fmt.Fprintf(str, "Processed %d bytes, %.3f megabytes (%s)\n", sumBytes, size, strings.ToUpper(SizeUnit))1420 }1421}14221423func isLeapYear(year int) bool {1424 leapFlag := false1425 if year%4 == 0 {1426 if year%100 == 0 {1427 leapFlag = year%400 == 01428 } else {1429 leapFlag = true1430 }1431 }1432 return leapFlag1433}14341435func aggregateLanguageSummary(input chan *FileJob) []LanguageSummary {1436 langs := map[string]LanguageSummary{}14371438 for res := range input {1439 _, ok := langs[res.Language]14401441 if !ok {1442 files := []*FileJob{}1443 if Files {1444 files = append(files, res)1445 }14461447 langs[res.Language] = LanguageSummary{1448 Name: res.Language,1449 Lines: res.Lines,1450 Code: res.Code,1451 Comment: res.Comment,1452 Blank: res.Blank,1453 Complexity: res.Complexity,1454 Count: 1,1455 Files: files,1456 Bytes: res.Bytes,1457 ULOC: 0,1458 }1459 } else {1460 tmp := langs[res.Language]1461 files := tmp.Files1462 if Files {1463 files = append(files, res)1464 }14651466 langs[res.Language] = LanguageSummary{1467 Name: res.Language,1468 Lines: tmp.Lines + res.Lines,1469 Code: tmp.Code + res.Code,1470 Comment: tmp.Comment + res.Comment,1471 Blank: tmp.Blank + res.Blank,1472 Complexity: tmp.Complexity + res.Complexity,1473 Count: tmp.Count + 1,1474 Files: files,1475 Bytes: res.Bytes + tmp.Bytes,1476 ULOC: 0,1477 }1478 }1479 }14801481 language := make([]LanguageSummary, 0, len(langs))1482 for _, summary := range langs {1483 summary.ULOC = len(ulocLanguageCount[summary.Name]) // for #4981484 language = append(language, summary)1485 }14861487 return language1488}14891490func sortLanguageSummary(language []LanguageSummary) []LanguageSummary {1491 // Cater for the common case of adding plural even for those options that don't make sense1492 // as it's quite common for those who English is not a first language to make a simple mistake1493 // NB in any non name cases if the values are the same we sort by name to ensure1494 // deterministic output1495 switch SortBy {1496 case "name", "names", "language", "languages", "lang", "langs":1497 slices.SortFunc(language, func(a, b LanguageSummary) int {1498 return strings.Compare(a.Name, b.Name)1499 })1500 case "line", "lines":1501 slices.SortFunc(language, func(a, b LanguageSummary) int {1502 if order := cmp.Compare(b.Lines, a.Lines); order != 0 {1503 return order1504 }1505 return strings.Compare(a.Name, b.Name)1506 })1507 case "blank", "blanks":1508 slices.SortFunc(language, func(a, b LanguageSummary) int {1509 if order := cmp.Compare(b.Blank, a.Blank); order != 0 {1510 return order1511 }1512 return strings.Compare(a.Name, b.Name)1513 })1514 case "code", "codes":1515 slices.SortFunc(language, func(a, b LanguageSummary) int {1516 if order := cmp.Compare(b.Code, a.Code); order != 0 {1517 return order1518 }1519 return strings.Compare(a.Name, b.Name)1520 })1521 case "comment", "comments":1522 slices.SortFunc(language, func(a, b LanguageSummary) int {1523 if order := cmp.Compare(b.Comment, a.Comment); order != 0 {1524 return order1525 }1526 return strings.Compare(a.Name, b.Name)1527 })1528 case "complexity", "complexitys":1529 slices.SortFunc(language, func(a, b LanguageSummary) int {1530 if order := cmp.Compare(b.Complexity, a.Complexity); order != 0 {1531 return order1532 }1533 return strings.Compare(a.Name, b.Name)1534 })1535 case "byte", "bytes":1536 slices.SortFunc(language, func(a, b LanguageSummary) int {1537 if order := cmp.Compare(b.Bytes, a.Bytes); order != 0 {1538 return order1539 }1540 return strings.Compare(a.Name, b.Name)1541 })1542 case "file", "files":1543 slices.SortFunc(language, func(a, b LanguageSummary) int {1544 if order := cmp.Compare(b.Count, a.Count); order != 0 {1545 return order1546 }1547 return strings.Compare(a.Name, b.Name)1548 })1549 default: // Files IE default falls into this category1550 slices.SortFunc(language, func(a, b LanguageSummary) int {1551 if order := cmp.Compare(b.Count, a.Count); order != 0 {1552 return order1553 }1554 return strings.Compare(a.Name, b.Name)1555 })1556 }15571558 return language1559}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.