processor/formatters.go GO 1,560 lines View on github.com → Search inside
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}

Code quality findings 80

Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_ = w.Write(record)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_ = w.Write(record)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_ = w.WriteAll(recordsEnd)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "files", result.Name, result.Count)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "lines", result.Name, result.Lines)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "code", result.Name, result.Code)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "comments", result.Name, result.Comment)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "blanks", result.Name, result.Blank)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "complexity", result.Name, result.Complexity)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsSummaryRecordFormat, "bytes", result.Name, result.Bytes)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "lines", file.Language, filename, file.Lines)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "code", file.Language, filename, file.Code)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "comments", file.Language, filename, file.Comment)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "blanks", file.Language, filename, file.Blank)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "complexity", file.Language, filename, file.Complexity)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(sb, openMetricsFileRecordFormat, "bytes", file.Language, filename, file.Bytes)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, `<tr>
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, `<tr>
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, `</tbody>
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, `
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, `
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, "\ninsert into t values('%s', '%s', '%s', '%s', '%s', %d, %d, %d, %d, %d, %d);",
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, "\ninsert into metadata values('%s', '%s', %f, %f, %f, %f);",
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, "\ninsert into locomo_metadata values('%s', '%s', %f, %f, %f, %f, %f, '%s', %f);",
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_ = toCSVStream(i)
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularWideFormatHead, "Language", "Files", "Lines", "Blanks", "Comments", "Code", "Complexity", "Complexity/Lines")
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularWideFormatBody, trimmedName, summary.Count, summary.Lines, summary.Blank, summary.Comment, summary.Code, summary.Complexity, summary.WeightedComplexity)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str,
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularWideFormatFileMaxMean, maxIn(summary.LineLength), meanIn(summary.LineLength))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularWideUlocLanguageFormatBody, len(ulocLanguageCount[summary.Name]))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularWideFormatFile, tmp, res.Lines, res.Blank, res.Comment, res.Code, res.Complexity, res.WeightedComplexity)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularWideFormatBody, "Total", sumFiles, sumLines, sumBlank, sumComment, sumCode, sumComplexity, sumWeightedComplexity)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularWideUlocGlobalFormatBody, len(ulocGlobalCount))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularWideDrynessFormatBody, dryness)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularShortFormatHead, "Language", "Files", "Lines", "Blanks", "Comments", "Code", "Complexity")
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, tabularShortFormatHeadNoComplexity, "Language", "Files", "Lines", "Blanks", "Comments", "Code")
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortFormatBody, trimmedName, summary.Count, summary.Lines, summary.Blank, summary.Comment, summary.Code, summary.Complexity)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortFormatBodyNoComplexity, trimmedName, summary.Count, summary.Lines, summary.Blank, summary.Comment, summary.Code)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str,
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str,
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortFormatFileMaxMean, maxIn(summary.LineLength), meanIn(summary.LineLength))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortFormatFileMaxMeanNoComplexity, maxIn(summary.LineLength), meanIn(summary.LineLength))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortFormatFile, tmp, res.Lines, res.Blank, res.Comment, res.Code, res.Complexity)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortFormatFileNoComplexity, tmp, res.Lines, res.Blank, res.Comment, res.Code)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortUlocLanguageFormatBody, len(ulocLanguageCount[summary.Name]))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortUlocLanguageFormatBodyNoComplexity, len(ulocLanguageCount[summary.Name]))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortFormatBody, "Total", sumFiles, sumLines, sumBlank, sumComment, sumCode, sumComplexity)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortFormatBodyNoComplexity, "Total", sumFiles, sumLines, sumBlank, sumComment, sumCode)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortUlocGlobalFormatBody, len(ulocGlobalCount))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, tabularShortDrynessFormatBody, dryness)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Total Physical Source Lines of Code (SLOC) = %d\n", sumCode)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Development Effort Estimate, Person-Years (Person-Months) = %.2f (%.2f)\n", estimatedEffort/12, estimatedEffort)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " (Basic COCOMO model, Person-Months = %.2f*(KSLOC**%.2f)*%.2f)\n", projectType[CocomoProjectType][0], projectType[CocomoProjectType][1], EAF)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Schedule Estimate, Years (Months) = %.2f (%.2f)\n", estimatedScheduleMonths/12, estimatedScheduleMonths)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " (Basic COCOMO model, Months = %.2f*(person-months**%.2f))\n", projectType[CocomoProjectType][2], projectType[CocomoProjectType][3])
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Estimated Average Number of Developers (Effort/Schedule) = %.2f\n", estimatedPeopleRequired)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Total Estimated Cost to Develop = %s%.0f\n", CurrencySymbol, estimatedCost)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " (average salary = %s%d/year, overhead = %.2f)\n", CurrencySymbol, AverageWage, Overhead)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Estimated Cost to Develop (%s) %s%d\n", CocomoProjectType, CurrencySymbol, int64(estimatedCost))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Estimated Schedule Effort (%s) %.2f months\n", CocomoProjectType, estimatedScheduleMonths)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Estimated People Required 1 Grandparent\n")
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "Estimated People Required (%s) %.2f\n", CocomoProjectType, estimatedPeopleRequired)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, "LOCOMO LLM Cost Estimate (%s)\n", result.Preset)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " Tokens Required (in/out) %.1fM / %.1fM\n", result.InputTokens/1_000_000, result.OutputTokens/1_000_000)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " Cost to Generate %s%.0f\n", CurrencySymbol, result.Cost)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " Estimated Cycles %.1f\n", result.IterationFactor)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " Generation Time (serial) %.1f days\n", result.GenerationSeconds/86400)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " Generation Time (serial) %.1f hours\n", result.GenerationSeconds/3600)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " Generation Time (serial) %.1f minutes\n", result.GenerationSeconds/60)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = p.Fprintf(str, " Human Review Time %.1f hours\n", result.ReviewHours)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, "Processed %d bytes, %s megabytes (%s)\n", sumBytes, `¯\_(ツ)_/¯`, strings.ToUpper(SizeUnit))
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(str, "Processed %d bytes, %.3f megabytes (%s)\n", sumBytes, size, strings.ToUpper(SizeUnit))
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
records = append(records, []string{
Unstructured output; use a structured logging library (e.g., slog, zap, zerolog, logrus)
info correctness fmt-println
fmt.Println("Language,Provider,Filename,Lines,Code,Comments,Blanks,Complexity,Bytes,Uloc")
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
var quoteRegex = regexp.MustCompile("\"")
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%s,%s,%s,%d,%d,%d,%d,%d,%d,%d\n",
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
results = append(results, res)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%s unable to be written to for format %s: %s", t[1], t[0], err)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
files = append(files, res)

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.