processor/processor.go GO 1,233 lines View on github.com → Search inside
1// SPDX-License-Identifier: MIT23package processor45import (6	"fmt"7	"io"8	"os"9	"path/filepath"10	"regexp"11	"runtime"12	"runtime/debug"13	"slices"14	"strconv"15	"strings"16	"sync"1718	"github.com/boyter/gocodewalker"19)2021// Version indicates the version of the application22var Version = "4.1.0"2324// Flags set via the CLI which control how the output is displayed2526// Files indicates if there should be file output or not when formatting27var Files = false2829// Languages indicates if the command line should print out the supported languages30var Languages = false3132// ListCounters indicates if the command line should print out the languages that33// have a scanner written for them rather than being counted by the generic loop.34// Sixteen names is unreadable in a flag help string, so the flag names the count35// and this prints the list.36var ListCounters = false3738// Verbose enables verbose logging output39var Verbose = false4041// Debug enables debug logging output42var Debug = false4344// Trace enables trace logging output which is extremely verbose45var Trace = false4647// Duplicates enables duplicate file detection48var Duplicates = false4950// MinifiedGenerated enables minified/generated file detection51var MinifiedGenerated = false5253// IgnoreMinifiedGenerate printing counts for minified/generated files54var IgnoreMinifiedGenerate = false5556// MinifiedGeneratedLineByteLength number of bytes per average line to determine file is minified/generated57var MinifiedGeneratedLineByteLength = 2555859// Minified enables minified file detection60var Minified = false6162// IgnoreMinified ignore printing counts for minified files63var IgnoreMinified = false6465// Generated enables generated file detection66var Generated = false6768// GeneratedMarkers defines head markers for generated file detection69var GeneratedMarkers []string7071// IgnoreGenerated ignore printing counts for generated files72var IgnoreGenerated = false7374// Complexity toggles complexity calculation75var Complexity = false7677// Cognitive toggles cognitive (nesting-weighted) complexity calculation78var Cognitive = false7980// More enables wider output with more information in formatter81var More = false8283// Cocomo toggles the COCOMO calculation84var Cocomo = false8586// SLOCCountFormat prints a more SLOCCount like COCOMO calculation87var SLOCCountFormat = false8889// CocomoProjectType allows the flipping between project types which impacts the calculation90var CocomoProjectType = "organic"9192// Size toggles the Size calculation93var Size = false9495// Draw horizontal borders between sections.96var HBorder = false9798// SizeUnit determines what size calculation is used for megabytes99var SizeUnit = "si"100101// Ci indicates if running inside a CI so to disable box drawing characters102var Ci = false103104// GitIgnore disables .gitignore checks105var GitIgnore = false106107// GitModuleIgnore disables .gitmodules checks108var GitModuleIgnore = false109110// Ignore disables ignore file checks111var Ignore = false112113// SccIgnore disables sccignore file checks114var SccIgnore = false115116// CountIgnore should we count ignore files?117var CountIgnore = false118119// CountUnsupported when set counts files scc does not recognise under an120// "Unknown" category, treating them as plain text. See issue #464.121var CountUnsupported = false122123// IgnoreFiles are paths to additional ignore files supplied via --ignore-file.124// They are applied as a low priority base layer in the order supplied so a later125// file can override an earlier one, and any in-tree .gitignore/.ignore/.sccignore126// discovered while walking overrides all of them.127var IgnoreFiles = []string{}128129// DisableCheckBinary toggles checking for binary files using NUL bytes130var DisableCheckBinary = false131132// UlocMode toggles checking for binary files using NUL bytes133var UlocMode = false134135// Percent toggles checking for binary files using NUL bytes136var Percent = false137138// MaxMean sets the calculation of the max and mean line length139var MaxMean = false140141// Dryness toggles checking for binary files using NUL bytes142var Dryness = false143144// SortBy sets which column output in formatter should be sorted by145var SortBy = ""146147// Exclude is a regular expression which is used to exclude files from being processed148var Exclude = []string{}149150// CountAs is a rule for mapping known or new extensions to other rules151var CountAs = ""152153// Format sets the output format of the formatter154var Format = ""155156// FormatMulti is a rule for defining multiple output formats157var FormatMulti = ""158159// SQLProject is used to store the name for the SQL insert formats but is optional160var SQLProject = ""161162// RemapUnknown allows remapping of unknown files with a string to search the content for163var RemapUnknown = ""164165// RemapAll allows remapping of all files with a string to search the content for166var RemapAll = ""167168type remapRule struct {169	pattern  []byte170	language string171}172173type remapConfig struct {174	all     []remapRule175	unknown []remapRule176}177178type processorContext struct {179	remap remapConfig180	// summary, when set, tells the counting workers to fold their own results181	// into it instead of sending every file down the summary channel. Only the182	// default and wide summaries can use it; see summariseInWorkers.183	summary *sharedSummaryTotals184}185186func parseRemapRules(value string) []remapRule {187	rules := []remapRule{}188189	for s := range strings.SplitSeq(value, ",") {190		t := strings.Split(s, ":")191		if len(t) == 2 {192			rules = append(rules, remapRule{193				pattern:  []byte(t[0]),194				language: t[1],195			})196		}197	}198199	return rules200}201202func newRemapConfig(remapAll string, remapUnknown string) remapConfig {203	c := remapConfig{204		all:     parseRemapRules(remapAll),205		unknown: parseRemapRules(remapUnknown),206	}207208	// Load the features for every language a rule can remap to. Remapping only209	// sets job.Language, and in lazy mode (which is every CLI run) nothing else210	// guarantees that language was ever loaded — CountStats would then find no211	// features and count the file as plain text, with no comments and no212	// complexity. Done once here at setup rather than in the remap functions213	// themselves, which run per file on the hot path.214	for _, rule := range c.all {215		LoadLanguageFeature(rule.language)216	}217	for _, rule := range c.unknown {218		LoadLanguageFeature(rule.language)219	}220221	return c222}223224// MatchEngine selects how a CountRule pattern is interpreted. Glob is the225// default; regex is opt-in via the re: prefix.226type MatchEngine int227228const (229	// MatchGlob is the default. The pattern is a glob ('*' and '?') translated230	// to an anchored regex and matched as a full match against the path.231	MatchGlob MatchEngine = iota232	// MatchRegex treats the pattern as a raw (unanchored) RE2 regex. Opt in233	// with the re: prefix.234	MatchRegex235)236237// CountRule is the typed, library-facing form of a --count-as-pattern rule.238// It matches files by their path and relabels them to a new named category239// whose counting rules are cloned from an existing base language.240type CountRule struct {241	Engine       MatchEngine // MatchGlob (the default) or MatchRegex242	Pattern      string      // glob or regex source243	Name         string      // new category display name244	BaseLanguage string      // existing language whose counting rules are cloned245}246247// CountRules is the typed input set either directly by library users or by the248// CLI after parsing CountAsPattern. Setup happens in setupCountRules.249var CountRules []CountRule250251// CountAsPattern holds the raw repeatable --count-as-pattern flag values. Each252// is parsed into a CountRule at setup. Library users may set CountRules directly.253var CountAsPattern []string254255// compiledCountRule is the runtime form scanned by newFileJob256type compiledCountRule struct {257	re   *regexp.Regexp258	name string259}260261var compiledCountRules []compiledCountRule262263// CurrencySymbol allows setting the currency symbol for cocomo project cost estimation264var CurrencySymbol = ""265266// FileOutput sets the file that output should be written to267var FileOutput = ""268269// PathDenyList sets the paths that should be skipped270var PathDenyList = []string{}271272// FileListQueueSize is the queue of files found and ready to be read into memory273var FileListQueueSize = runtime.NumCPU()274275// FileProcessJobWorkers is the number of workers that process the file collecting stats276var FileProcessJobWorkers = runtime.NumCPU() * 4277278// FileListJobWorkers is the number of workers that turn a path the walker found279// into a FileJob, which is a stat and a language lookup each280var FileListJobWorkers = runtime.NumCPU()281282// FileSummaryJobQueueSize is the queue used to hold processed file statistics before formatting283var FileSummaryJobQueueSize = runtime.NumCPU()284285// DirectoryWalkerJobWorkers is the number of workers which will walk the directory tree286var DirectoryWalkerJobWorkers = 8287288// AllowListExtensions is a list of extensions which are allowed to be processed289var AllowListExtensions = []string{}290291// ExcludeListExtensions is a list of extensions which should be ignored292var ExcludeListExtensions = []string{}293294// ExcludeFilename is a list of filenames which should be ignored295var ExcludeFilename = []string{}296297// AverageWage is the average wage in dollars used for the COCOMO cost estimate298var AverageWage int64 = 56286299300// Overhead is the overhead multiplier for corporate overhead (facilities, equipment, accounting, etc.)301var Overhead float64 = 2.4302303// EAF is the effort adjustment factor derived from the cost drivers, i.e. 1.0 if rated nominal304var EAF float64 = 1.0305306// Locomo toggles the LOCOMO (LLM Output COst MOdel) calculation307var Locomo = false308309// CostComparison enables both COCOMO and LOCOMO output for side-by-side comparison310var CostComparison = false311312// LocomoPresetName is the LLM model preset for pricing and throughput defaults313var LocomoPresetName = "medium"314315// LocomoInputPrice is the cost per 1M input tokens (overrides preset)316var LocomoInputPrice float64317var LocomoInputPriceSet = false318319// LocomoOutputPrice is the cost per 1M output tokens (overrides preset)320var LocomoOutputPrice float64321var LocomoOutputPriceSet = false322323// LocomoTPS is the output tokens per second (overrides preset)324var LocomoTPS float64325var LocomoTPSSet = false326327// LocomoReviewMinutesPerLine is the human review time per line of code in minutes328var LocomoReviewMinutesPerLine float64 = 0.01329330// LocomoConfig is the power-user config string "tokensPerLine,baseInputPerLine,complexityWeight,iterations,iterationWeight"331var LocomoConfig = ""332333// LocomoTokensPerLine is the average number of output tokens per line of code334var LocomoTokensPerLine float64 = 10335336// LocomoBaseInputPerLine is the base number of input tokens per output line337var LocomoBaseInputPerLine float64 = 20338339// LocomoComplexityWeight is the scaling weight applied to sqrt(complexity density) for input tokens340var LocomoComplexityWeight float64 = 5341342// LocomoIterations is the base number of iteration/retry attempts343var LocomoIterations float64 = 1.5344345// LocomoIterationWeight is the scaling weight for complexity-driven retries346var LocomoIterationWeight float64 = 2347348// LocomoCyclesOverride is the user-supplied iteration factor override (--locomo-cycles)349var LocomoCyclesOverride float64350351// LocomoCyclesSet indicates whether --locomo-cycles was explicitly set352var LocomoCyclesSet = false353354// GcFileCount is the number of files to process before turning the GC back on355var GcFileCount = 10000356var gcPercent = -1357var isLazy = false358359// NoLarge if set true will ignore files over a certain number of lines or bytes360var NoLarge = false361362// IncludeSymLinks if set true will count symlink files363var IncludeSymLinks = false364365// LargeLineCount number of lines before being counted as a large file based on https://github.com/pinpt/ripsrc/blob/master/ripsrc/fileinfo/fileinfo.go#L44366var LargeLineCount int64 = 40000367368// LargeByteCount number of bytes before being counted as a large file based on https://github.com/pinpt/ripsrc/blob/master/ripsrc/fileinfo/fileinfo.go#L44369var LargeByteCount int64 = 1000000370371// Hotspots toggles the hotspots git-history report372var Hotspots = false373374// Coupling toggles the change-coupling git-history report (file pairs that375// change together)376var Coupling = false377378// CouplingFor, when non-empty, switches the coupling report to the379// file-oriented "blast radius" view for the given path: what tends to change380// when that file changes.381var CouplingFor = ""382383// CouplingWeighted ranks coupling by degree × the pair's (smaller) file384// complexity instead of raw co-change, so pairs of genuinely complex files385// outrank generated/data-file churn. Implies Coupling. Honours the Cognitive386// global for its complexity source, matching --hotspots.387var CouplingWeighted = false388389// ByAuthor toggles the author-rollup git-history report390var ByAuthor = false391392// Timeline selects an over-time view. With ByAuthor, runs the author393// timeline report (plan 04); alone, runs the languages-over-time report394// (plan 05). With Hotspots set, the combination errors out.395var Timeline = false396397// HistoryBuckets is the time-bucket resolution for the timeline reports.398// Wired to --buckets in main.go; default 60.399var HistoryBuckets = 60400401// FoldAuthors enables the name+domain identity folding fallback applied402// after the mailmap. Toggled off via --no-fold-authors.403var FoldAuthors = true404405// DirFilePaths is not set via flags but by arguments following the flags for file or directory to process406var DirFilePaths = []string{}407408// ExtensionToLanguage is loaded from the JSON that is in constants.go409var ExtensionToLanguage = map[string][]string{}410411// ShebangLookup loaded from the JSON in constants.go contains shebang lookups412var ShebangLookup = map[string][]string{}413414// FilenameToLanguage similar to ExtensionToLanguage loaded from the JSON in constants.go415var FilenameToLanguage = map[string]string{}416417// LanguageFeatures contains the processed languages from processLanguageFeature418var LanguageFeatures = map[string]LanguageFeature{}419420// LanguageFeaturesMutex is the shared mutex used to control getting and setting of language features421// used rather than sync.Map because it turned out to be marginally faster422var LanguageFeaturesMutex = sync.Mutex{}423424// Start time in milli seconds in case we want the total time425var startTimeMilli = makeTimestampMilli()426427// ConfigureGc needs to be set outside of ProcessConstants because it should only be enabled in command line428// mode https://github.com/boyter/scc/issues/32429func ConfigureGc() {430	gcPercent = debug.SetGCPercent(gcPercent)431}432433// EnableGc restores the garbage collector to the percentage captured by ConfigureGc.434func EnableGc() {435	if gcPercent != -1 {436		debug.SetGCPercent(gcPercent)437	}438}439440// ConfigureLazy is a simple setter used to turn on lazy loading used only by command line441func ConfigureLazy(lazy bool) {442	isLazy = lazy443}444445// ProcessConstants is responsible for setting up the language features based on the JSON file that is stored in constants446// Needs to be called at least once in order for anything to actually happen447func ProcessConstants() {448	startTime := makeTimestampNano()449450	// Reset the reverse-lookup maps so ProcessConstants is idempotent. The451	// ExtensionToLanguage entries are built with append, so without clearing452	// first a repeated call (the long-lived MCP server invokes ProcessConstants453	// once per tool call) would accumulate duplicate languages for every454	// extension. scc was historically a one-shot CLI where this ran exactly455	// once, so it never surfaced until server mode.456	clear(ExtensionToLanguage)457	clear(FilenameToLanguage)458	clear(ShebangLookup)459460	for name, value := range languageDatabase {461		for _, ext := range value.Extensions {462			ExtensionToLanguage[ext] = append(ExtensionToLanguage[ext], name)463		}464465		for _, fname := range value.FileNames {466			FilenameToLanguage[fname] = name467		}468469		if len(value.SheBangs) != 0 {470			ShebangLookup[name] = value.SheBangs471		}472	}473474	// If we have anything in CountAs set it up now475	if len(CountAs) != 0 {476		setupCountAs()477	}478479	printTraceF("nanoseconds build extension to language: %d", makeTimestampNano()-startTime)480481	// Set up any path pattern count rules, minting new categories backed by a482	// base language. The function clones the base language and builds its483	// features so counting works in both lazy and non-lazy modes.484	if len(CountAsPattern) != 0 || len(CountRules) != 0 {485		setupCountRules()486	}487488	// Configure COCOMO setting. projectType is keyed by the canonical489	// lowercase name, so when the selection matches a built-in type we must490	// normalize CocomoProjectType to that lowercase key — otherwise the491	// membership check below passes for "Organic" while the real lookups in492	// EstimateEffort/EstimateScheduleMonths index projectType["Organic"], hit493	// a nil slice and panic. See `scc --cocomo-project-type Organic`.494	if _, ok := projectType[strings.ToLower(CocomoProjectType)]; ok {495		CocomoProjectType = strings.ToLower(CocomoProjectType)496	} else {497		// let's see if we can turn it into a custom one498		spl := strings.Split(CocomoProjectType, ",")499		val := []float64{}500		if len(spl) == 5 {501			// let's try to convert to float if we can502			for i := 1; i < 5; i++ {503				f, err := strconv.ParseFloat(spl[i], 64)504				if err == nil {505					val = append(val, f)506				}507			}508		}509510		if len(val) == 4 {511			projectType[CocomoProjectType] = val512		} else {513			// if nothing matches fall back to organic514			CocomoProjectType = "organic"515		}516	}517518	// If lazy is set then we want to load in the features as we find them not in one go519	// however otherwise being used as a library so just load them all in520	if !isLazy {521		startTime = makeTimestampMilli()522		for name, value := range languageDatabase {523			processLanguageFeature(name, value)524		}525526		printTraceF("milliseconds build language features: %d", makeTimestampMilli()-startTime)527	} else {528		printTrace("configured to lazy load language features")529	}530531	// Fix for https://github.com/boyter/scc/issues/250532	fixedPath := make([]string, 0, len(PathDenyList))533	for _, path := range PathDenyList {534		fixedPath = append(fixedPath, strings.TrimRight(path, "/"))535	}536	PathDenyList = fixedPath537}538539// Configure and setup any count-as params the use has supplied540func setupCountAs() {541	for s := range strings.SplitSeq(CountAs, ",") {542		t := strings.Split(s, ":")543		if len(t) != 2 {544			printError(fmt.Sprintf("ignoring malformed count-as rule %q: expected format <from>:<to>", s))545			continue546		}547548		// There are two cases here.549		// first is they provide the name e.g. "Cargo Lock"550		// second is that the user supplies the extension EG wsdl551		// we should support BOTH cases552		// always remember we only need to validate t[1] as that's the one553		// that tells us where we are trying to map554		target, ok := resolveBaseLanguage(t[1])555		if ok {556			ExtensionToLanguage[strings.ToLower(t[0])] = []string{target}557			printDebugF("set to count extension: %s as language %s", t[0], target)558			continue559		}560561		// The target t[1] matched neither a known language name nor a known562		// extension, so no mapping was registered. Warn rather than silently563		// ignoring the rule, since count-as cannot mint new categories yet.564		printError(fmt.Sprintf("ignoring count-as rule %q: target %q is not a known language or extension", s, t[1]))565	}566}567568// resolveBaseLanguage resolves a user supplied target to a canonical language569// name. It first tries to match a language name (most reliable as names are570// unique) and falls back to matching a known extension. Returns the canonical571// language name and whether it was resolved.572func resolveBaseLanguage(target string) (string, bool) {573	// Match by language name which is the most reliable as the name is unique574	for name := range languageDatabase {575		if strings.EqualFold(name, target) {576			return name, true577		}578	}579580	// Fall back to extension match, note this is less reliable as some581	// languages share extensions so we take the first registered language582	langs, ok := ExtensionToLanguage[strings.ToLower(target)]583	if ok && len(langs) != 0 {584		return langs[0], true585	}586587	return "", false588}589590// parseCountAsPattern parses a single --count-as-pattern rule of the form591// [engine:]pattern:name:baselang into a CountRule.592//593// The engine prefix is optional and the pattern is treated as a GLOB BY594// DEFAULT; prefix with re: to opt into a regex (or glob: to be explicit). We595// keep glob and regex as distinct modes rather than inferring, because the same596// string is valid in both engines with different meaning (e.g. "foo.rb" matches597// only foo.rb as a glob but also fooXrb as a regex), so guessing would silently598// match the wrong files.599//600// Because regex patterns and paths legitimately contain ':', name and baselang601// are peeled from the right and the pattern is whatever remains in between.602func parseCountAsPattern(s string) (CountRule, error) {603	engine := MatchGlob604	rest := s605606	switch {607	case strings.HasPrefix(rest, "re:"):608		engine = MatchRegex609		rest = rest[len("re:"):]610	case strings.HasPrefix(rest, "glob:"):611		engine = MatchGlob612		rest = rest[len("glob:"):]613	}614615	// baselang = after the last ':', name = between the 2nd-last and last ':'616	lastColon := strings.LastIndex(rest, ":")617	if lastColon == -1 {618		return CountRule{}, fmt.Errorf("expected format [engine:]pattern:name:baselang")619	}620	baseLanguage := rest[lastColon+1:]621622	nameColon := strings.LastIndex(rest[:lastColon], ":")623	if nameColon == -1 {624		return CountRule{}, fmt.Errorf("expected format [engine:]pattern:name:baselang")625	}626	name := rest[nameColon+1 : lastColon]627	pattern := rest[:nameColon]628629	if pattern == "" || name == "" || baseLanguage == "" {630		return CountRule{}, fmt.Errorf("pattern, name and baselang must all be non-empty")631	}632633	return CountRule{Engine: engine, Pattern: pattern, Name: name, BaseLanguage: baseLanguage}, nil634}635636// globToRegex converts a simple glob into an anchored regex. Glob is the637// default --count-as-pattern engine. Only '*' (any run of characters) and '?'638// (single character) are special, everything else is matched literally. The639// result is anchored as a full match.640func globToRegex(glob string) string {641	var b strings.Builder642	b.WriteByte('^')643	for _, r := range glob {644		switch r {645		case '*':646			b.WriteString(".*")647		case '?':648			b.WriteByte('.')649		default:650			b.WriteString(regexp.QuoteMeta(string(r)))651		}652	}653	b.WriteByte('$')654	return b.String()655}656657// setupCountRules parses CountAsPattern into CountRules, compiles each rule and658// registers a cloned language under its new name so counting works. Invalid659// rules are reported to stderr and skipped, consistent with --count-as.660func setupCountRules() {661	for _, s := range CountAsPattern {662		rule, err := parseCountAsPattern(s)663		if err != nil {664			printError(fmt.Sprintf("ignoring malformed count-as-pattern rule %q: %s", s, err))665			continue666		}667		CountRules = append(CountRules, rule)668	}669670	for _, rule := range CountRules {671		base, ok := resolveBaseLanguage(rule.BaseLanguage)672		if !ok {673			printError(fmt.Sprintf("ignoring count-as-pattern rule for %q: base language %q is not a known language or extension", rule.Name, rule.BaseLanguage))674			continue675		}676677		source := rule.Pattern678		if rule.Engine == MatchGlob {679			source = globToRegex(rule.Pattern)680		}681682		re, err := regexp.Compile(source)683		if err != nil {684			printError(fmt.Sprintf("ignoring count-as-pattern rule for %q: invalid pattern %q: %s", rule.Name, rule.Pattern, err))685			continue686		}687688		// Clone the base language under the new name so it has counting rules,689		// clearing the matchers so the minted category never participates in690		// normal extension/filename/shebang detection.691		cloned := languageDatabase[base]692		cloned.Extensions = nil693		cloned.FileNames = nil694		cloned.SheBangs = nil695		languageDatabase[rule.Name] = cloned696697		// Populate features now in non-lazy mode, otherwise LoadLanguageFeature698		// will build them on first use since the name is in languageDatabase.699		if !isLazy {700			processLanguageFeature(rule.Name, cloned)701		}702703		compiledCountRules = append(compiledCountRules, compiledCountRule{re: re, name: rule.Name})704		printDebugF("set to count path matching %q as new language %s based on %s", rule.Pattern, rule.Name, base)705	}706}707708// LoadLanguageFeature will load a single feature as requested given the name709func LoadLanguageFeature(loadName string) {710	if !isLazy {711		return712	}713714	// Check if already loaded and if so return because we don't need to do it again715	LanguageFeaturesMutex.Lock()716	_, ok := LanguageFeatures[loadName]717	LanguageFeaturesMutex.Unlock()718	if ok {719		return720	}721722	// A plain map lookup, not a range-with-break: ranging leaves value set to723	// whatever the iteration happened to land on last when loadName is absent,724	// which would register some arbitrary language's comment and complexity725	// rules under this name. An unknown name has no features to build, so a726	// miss must be a no-op — CountStats then treats the file as plain text.727	value, ok := languageDatabase[loadName]728	if !ok {729		return730	}731732	startTime := makeTimestampNano()733	processLanguageFeature(loadName, value)734	printTraceF("nanoseconds to build language %s features: %d", loadName, makeTimestampNano()-startTime)735}736737// caseSpellings returns the tokens as written, and for a language that reads738// its keywords in any case, every other spelling of them too. Batch and ASP739// open a comment with REM, and mean it however it is typed.740//741// Expanding here rather than folding case at match time keeps the counting loop742// exactly as it is: it walks a trie of bytes and knows nothing about letters. A743// token of n letters becomes 2^n entries, so the expansion is refused past a744// length that would be silly, which no comment token comes near.745func caseSpellings(tokens []string, caseInsensitive bool) []string {746	if !caseInsensitive {747		return tokens748	}749750	const maxLetters = 8751752	spellings := make([]string, 0, len(tokens))753	for _, token := range tokens {754		letters := 0755		for i := 0; i < len(token); i++ {756			if token[i] != asciiLower(token[i]) || token[i] != asciiUpper(token[i]) {757				letters++758			}759		}760761		if letters == 0 || letters > maxLetters {762			spellings = append(spellings, token)763			continue764		}765766		variants := []string{""}767		for i := 0; i < len(token); i++ {768			lower, upper := asciiLower(token[i]), asciiUpper(token[i])769			next := make([]string, 0, len(variants)*2)770			for _, prefix := range variants {771				next = append(next, prefix+string(lower))772				if upper != lower {773					next = append(next, prefix+string(upper))774				}775			}776			variants = next777		}778		spellings = append(spellings, variants...)779	}780781	return spellings782}783784func asciiLower(b byte) byte {785	if b >= 'A' && b <= 'Z' {786		return b + 'a' - 'A'787	}788789	return b790}791792func asciiUpper(b byte) byte {793	if b >= 'a' && b <= 'z' {794		return b - 'a' + 'A'795	}796797	return b798}799800func processLanguageFeature(name string, value Language) {801	complexityTrie := &Trie{}802	slCommentTrie := &Trie{}803	mlCommentTrie := &Trie{}804	stringTrie := &Trie{}805	tokenTrie := &Trie{}806	keywordBytes := make([][]byte, 0, len(value.Keywords))807	postfixExcludes := make([][]byte, 0, len(value.ComplexityChecksPostfixExcludes))808809	complexityMask := byte(0)810	singleLineCommentMask := byte(0)811	multiLineCommentMask := byte(0)812	stringMask := byte(0)813	processMask := byte(0)814815	for _, v := range value.ComplexityChecks {816		complexityMask |= v[0]817		complexityTrie.Insert(TComplexity, []byte(v))818		if !Complexity {819			tokenTrie.Insert(TComplexity, []byte(v))820		}821	}822	if !Complexity {823		processMask |= complexityMask824	}825826	for _, v := range value.ComplexityChecksPostfix {827		if !Complexity {828			tokenTrie.Insert(TComplexityPostfix, []byte(v))829			processMask |= v[0]830		}831	}832833	for _, v := range value.ComplexityChecksPostfixExcludes {834		postfixExcludes = append(postfixExcludes, []byte(v))835	}836837	for _, v := range caseSpellings(value.LineComment, value.CaseInsensitive) {838		singleLineCommentMask |= v[0]839		slCommentTrie.Insert(TSlcomment, []byte(v))840		tokenTrie.Insert(TSlcomment, []byte(v))841	}842	processMask |= singleLineCommentMask843844	for _, v := range value.MultiLine {845		multiLineCommentMask |= v[0][0]846		mlCommentTrie.InsertClose(TMlcomment, []byte(v[0]), []byte(v[1]))847		tokenTrie.InsertClose(TMlcomment, []byte(v[0]), []byte(v[1]))848	}849	processMask |= multiLineCommentMask850851	for _, v := range value.Quotes {852		stringMask |= v.Start[0]853		stringTrie.InsertClose(TString, []byte(v.Start), []byte(v.End))854		tokenTrie.InsertClose(TString, []byte(v.Start), []byte(v.End))855	}856	processMask |= stringMask857858	for _, v := range value.Keywords {859		keywordBytes = append(keywordBytes, []byte(v))860	}861862	// Compile any regex heuristics used to disambiguate shared extensions such863	// as .h between C / C++ / Objective-C. The patterns are validated at864	// generation time (scripts/include.go) so MustCompile is safe here, but we865	// guard with Compile anyway to honour the no-panics policy. Each pattern's866	// necessary literals are pre-converted to bytes so guessByHeuristics can867	// cheaply skip running the regex when none of them appear in the content.868	heuristics := make([]CompiledHeuristic, 0, len(value.Heuristics))869	for _, v := range value.Heuristics {870		re, err := regexp.Compile(v.Pattern)871		if err != nil {872			printWarnF("failed to compile heuristic %q for language %s: %v", v.Pattern, name, err)873			continue874		}875		literals := make([][]byte, 0, len(v.Literals))876		for _, l := range v.Literals {877			literals = append(literals, []byte(l))878		}879		heuristics = append(heuristics, CompiledHeuristic{Re: re, Literals: literals, Anchored: v.Anchored})880	}881882	// A line comment spelled as a word ends where the word does, which costs a883	// check on every token that matches. Almost no language has one, so work out884	// here whether this one does and let the hot loop skip the check entirely.885	escape := byte('\\')886	if len(value.Escape) != 0 {887		escape = value.Escape[0]888	}889890	// The exact set of bytes the counting loop has to stop on, which is every891	// byte that opens a token plus the newline and the nul. Built here once so892	// the loop pays a single load and branch per byte instead of a mask test893	// that nearly always passes followed by a failed trie walk.894	tokenFirst := newTokenFirst()895	for i := range tokenTrie.Table {896		if tokenTrie.Table[i] != nil {897			tokenFirst[i] = true898		}899	}900901	wordComments := false902	for _, token := range value.LineComment {903		if len(token) > 1 && isIdentifierContinue(token[len(token)-1]) {904			wordComments = true905			break906		}907	}908909	LanguageFeaturesMutex.Lock()910	LanguageFeatures[name] = LanguageFeature{911		Complexity:            complexityTrie,912		MultiLineComments:     mlCommentTrie,913		MultiLine:             value.MultiLine,914		SingleLineComments:    slCommentTrie,915		LineComment:           value.LineComment,916		Strings:               stringTrie,917		Tokens:                tokenTrie,918		Nested:                value.NestedMultiLine,919		LineSplice:            value.LineSplice,920		WordComments:          wordComments,921		CommentIsWord:         value.CommentIsWord,922		Escape:                escape,923		PostfixExcludes:       postfixExcludes,924		ComplexityCheckMask:   complexityMask,925		MultiLineCommentMask:  multiLineCommentMask,926		SingleLineCommentMask: singleLineCommentMask,927		StringCheckMask:       stringMask,928		ProcessMask:           processMask,929		TokenFirst:            tokenFirst,930		Keywords:              value.Keywords,931		KeywordBytes:          keywordBytes,932		Heuristics:            heuristics,933		Quotes:                value.Quotes,934	}935	LanguageFeaturesMutex.Unlock()936}937938func processFlags() {939	// If wide/more mode is enabled we want the complexity calculation940	// to happen regardless as that is the only purpose of the flag941	if More && Complexity {942		Complexity = false943	}944945	// If ignore minified/generated is on ensure we turn on the code to calculate that946	if IgnoreMinifiedGenerate {947		MinifiedGenerated = true948		IgnoreMinified = true949		IgnoreGenerated = true950	}951952	if MinifiedGenerated {953		Minified = true954		Generated = true955	}956957	if IgnoreMinified {958		Minified = true959	}960961	if IgnoreGenerated {962		Generated = true963	}964965	if Dryness {966		UlocMode = true967	}968969	printDebugF("Path Deny List: %v", PathDenyList)970	printDebugF("Sort By: %s", SortBy)971	printDebugF("White List: %v", AllowListExtensions)972	printDebugF("Files Output: %t", Files)973	printDebugF("Verbose: %t", Verbose)974	printDebugF("Duplicates Detection: %t", Duplicates)975	printDebugF("Complexity Calculation: %t", !Complexity)976	printDebugF("Wide: %t", More)977	// If cost-comparison is enabled, turn on both COCOMO and LOCOMO978	if CostComparison {979		Cocomo = false980		Locomo = true981	}982983	// LOCOMO needs complexity data to produce accurate estimates.984	// If complexity was disabled via --no-complexity, force it back on.985	if Locomo && Complexity {986		Complexity = false987	}988989	// Cognitive complexity is derived from the complexity tokens, so it needs990	// complexity counting enabled. Complexity is a disable switch, so force it991	// off (i.e. enable counting) even when --no-complexity was passed; cognitive992	// wins. Warn only when the user explicitly asked for both.993	if Cognitive && Complexity {994		Complexity = false995		printWarn("--no-complexity ignored because --cognitive requires complexity counting")996	}997998	printDebugF("Average Wage: %d", AverageWage)999	printDebugF("Cocomo: %t", !Cocomo)1000	printDebugF("Locomo: %t", Locomo)1001	printDebugF("Minified/Generated Detection: %t/%t", Minified, Generated)1002	printDebugF("Ignore Minified/Generated: %t/%t", IgnoreMinified, IgnoreGenerated)1003	printDebugF("IncludeSymLinks: %t", IncludeSymLinks)1004	printDebugF("Uloc: %t", UlocMode)1005	printDebugF("Dryness: %t", Dryness)1006}10071008// LanguageDatabase provides access to the internal language database1009// useful for consuming applications wanting to consume and use1010func LanguageDatabase() map[string]Language {1011	return languageDatabase1012}10131014func PrintLanguages(dst io.Writer) {1015	names := make([]string, 0, len(languageDatabase))1016	for key := range languageDatabase {1017		names = append(names, key)1018	}10191020	slices.SortFunc(names, func(a, b string) int {1021		return strings.Compare(strings.ToLower(a), strings.ToLower(b))1022	})10231024	for _, name := range names {1025		_, _ = fmt.Fprintf(dst, "%s (%s)\n", name, strings.Join(append(languageDatabase[name].Extensions, languageDatabase[name].FileNames...), ","))1026	}1027}10281029// global variables to deal with ULOC calculations1030var ulocMutex = sync.Mutex{}1031var ulocGlobalCount = map[string]struct{}{}1032var ulocLanguageCount = map[string]map[string]struct{}{}10331034// Process is the main entry point of the command line it sets everything up and starts running1035func Process() {1036	if Languages {1037		PrintLanguages(os.Stdout)1038		return1039	}10401041	if ListCounters {1042		PrintCounters(os.Stdout)1043		return1044	}10451046	ProcessConstants()1047	processFlags()1048	cleanVisitedPaths()1049	cleanDuplicates()10501051	// Clean up any invalid arguments before setting everything up1052	if len(DirFilePaths) == 0 {1053		DirFilePaths = append(DirFilePaths, ".")1054	}10551056	// --report mode short-circuits the normal format dispatch and writes a1057	// self-contained HTML report. Mutually exclusive with --format / -f: if1058	// the user passed both, warn on stderr and let --report win.1059	if ReportOut != "" {1060		if Format != "" && Format != "tabular" {1061			fmt.Fprintf(os.Stderr, "warning: --report overrides --format=%s\n", Format)1062		}1063		parseReportSkip(ReportSkip)1064		if len(DirFilePaths) > 1 {1065			fmt.Fprintf(os.Stderr, "warning: --report only analyses the first positional path (%s); other paths ignored\n", DirFilePaths[0])1066		}1067		if err := runReport(DirFilePaths); err != nil {1068			fmt.Fprintln(os.Stderr, err)1069			os.Exit(1)1070		}1071		return1072	}10731074	if Hotspots && (ByAuthor || Timeline) {1075		fmt.Fprintln(os.Stderr, "--hotspots is mutually exclusive with --by-author / --timeline; pick one report")1076		os.Exit(1)1077	}10781079	// --coupling-for implies the coupling report for a specific file, and1080	// --coupling-weighted is a modifier that implies the report too.1081	if CouplingFor != "" || CouplingWeighted {1082		Coupling = true1083	}10841085	// Coupling is a standalone report — it doesn't combine with any other.1086	if Coupling && (Hotspots || ByAuthor || Timeline) {1087		fmt.Fprintln(os.Stderr, "--coupling/--coupling-for is mutually exclusive with --hotspots / --by-author / --timeline; pick one report")1088		os.Exit(1)1089	}10901091	if Hotspots || Coupling || ByAuthor || Timeline {1092		if err := validateHistoryFlags(os.Stderr); err != nil {1093			fmt.Fprintln(os.Stderr, err)1094			os.Exit(1)1095		}1096	}10971098	if Hotspots {1099		if err := runHotspotsReport(DirFilePaths[0]); err != nil {1100			fmt.Fprintln(os.Stderr, err)1101			os.Exit(1)1102		}1103		return1104	}11051106	if Coupling {1107		if err := runCouplingReport(DirFilePaths[0]); err != nil {1108			fmt.Fprintln(os.Stderr, err)1109			os.Exit(1)1110		}1111		return1112	}11131114	if ByAuthor && Timeline {1115		if err := runAuthorTimelineReport(DirFilePaths[0]); err != nil {1116			fmt.Fprintln(os.Stderr, err)1117			os.Exit(1)1118		}1119		return1120	}11211122	if ByAuthor {1123		if err := runAuthorsReport(DirFilePaths[0]); err != nil {1124			fmt.Fprintln(os.Stderr, err)1125			os.Exit(1)1126		}1127		return1128	}11291130	if Timeline {1131		if err := runLanguagesTimelineReport(DirFilePaths[0]); err != nil {1132			fmt.Fprintln(os.Stderr, err)1133			os.Exit(1)1134		}1135		return1136	}11371138	filePaths := []string{}1139	dirPaths := []string{}11401141	// Check if the paths or files added exist and exit if not1142	for _, f := range DirFilePaths {1143		fpath := filepath.Clean(f)11441145		s, err := os.Stat(fpath)1146		if err != nil {1147			fmt.Println("file or directory could not be read: " + fpath)1148			os.Exit(1)1149		}11501151		if s.IsDir() {1152			dirPaths = append(dirPaths, fpath)1153		} else {1154			filePaths = append(filePaths, fpath)1155		}1156	}11571158	SortBy = strings.ToLower(SortBy)1159	ctx := processorContext{remap: newRemapConfig(RemapAll, RemapUnknown)}11601161	printDebugF("NumCPU: %d", runtime.NumCPU())1162	printDebugF("SortBy: %s", SortBy)1163	printDebugF("PathDenyList: %v", PathDenyList)11641165	potentialFilesQueue := make(chan []*gocodewalker.File, FileListQueueSize) // files that pass the .gitignore checks1166	fileListQueue := make(chan *FileJob, FileListQueueSize)                   // Files ready to be read from disk1167	fileSummaryJobQueue := make(chan *FileJob, FileSummaryJobQueueSize)       // Files ready to be summarised11681169	fileWalker := gocodewalker.NewParallelFileWalker(dirPaths, nil)1170	fileWalker.SetFileBatchQueue(potentialFilesQueue)1171	fileWalker.SetErrorHandler(func(e error) bool {1172		printError(e.Error())1173		return true1174	})1175	fileWalker.IgnoreGitIgnore = GitIgnore1176	fileWalker.IgnoreIgnoreFile = Ignore1177	fileWalker.IgnoreGitModules = GitModuleIgnore1178	fileWalker.IncludeHidden = true1179	fileWalker.ExcludeDirectory = PathDenyList1180	fileWalker.SetConcurrency(DirectoryWalkerJobWorkers)11811182	if !SccIgnore {1183		fileWalker.CustomIgnore = []string{".sccignore"}1184	}1185	fileWalker.CustomIgnoreFiles = IgnoreFiles11861187	var excludePathRegexes []*regexp.Regexp1188	for _, exclude := range Exclude {1189		regexpResult, err := regexp.Compile(exclude)1190		if err == nil {1191			fileWalker.ExcludeFilenameRegex = append(fileWalker.ExcludeFilenameRegex, regexpResult)1192			fileWalker.ExcludeDirectoryRegex = append(fileWalker.ExcludeDirectoryRegex, regexpResult)1193			excludePathRegexes = append(excludePathRegexes, regexpResult)1194		} else {1195			printError(err.Error())1196		}1197	}11981199	go func() {1200		err := fileWalker.Start()1201		if err != nil {1202			printError(err.Error())1203		}1204	}()12051206	startFileJobProducer(potentialFilesQueue, filePaths, excludePathRegexes, fileListQueue)12071208	// Where the output is one of the summaries that only ever prints per1209	// language totals, the workers add up their own results and merge once each1210	// at the end. That deletes a channel send and a channel receive per file1211	// from a stage where the receiver is a single goroutine, which is the1212	// difference between 86,000 handovers and one per worker.1213	if summariseInWorkers() {1214		ctx.summary = newSharedSummaryTotals()1215		workerSummary = ctx.summary1216	}12171218	go ctx.fileProcessorWorker(fileListQueue, fileSummaryJobQueue)12191220	result := fileSummarize(fileSummaryJobQueue)1221	if FileOutput == "" {1222		fmt.Print(result)1223	} else {1224		// A failed write must not report success: follow the --report path1225		// above and exit non-zero with the reason on stderr.1226		if err := os.WriteFile(FileOutput, []byte(result), 0644); err != nil {1227			fmt.Fprintln(os.Stderr, err)1228			os.Exit(1)1229		}1230		fmt.Println("results written to " + FileOutput)1231	}1232}

Code quality findings 30

Declared map variable without initialization; writing to a nil map causes a panic. Use make() to initialize
warning correctness nil-map-write
var ExtensionToLanguage = map[string][]string{}
Declared map variable without initialization; writing to a nil map causes a panic. Use make() to initialize
warning correctness nil-map-write
var ShebangLookup = map[string][]string{}
Declared map variable without initialization; writing to a nil map causes a panic. Use make() to initialize
warning correctness nil-map-write
var FilenameToLanguage = map[string]string{}
Declared map variable without initialization; writing to a nil map causes a panic. Use make() to initialize
warning correctness nil-map-write
var LanguageFeatures = map[string]LanguageFeature{}
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Ensure paired with Unlock defer to prevent deadlocks
warning correctness lock-without-unlock
LanguageFeaturesMutex.Lock()
Goroutine without waitgroup or channel; risks resource leaks or race conditions
warning correctness goroutine-without-sync
go func() {
Goroutine without waitgroup or channel; risks resource leaks or race conditions
warning correctness goroutine-without-sync
go ctx.fileProcessorWorker(fileListQueue, fileSummaryJobQueue)
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
pattern: []byte(t[0]),
Adjusting garbage collection settings dynamically can be a sign of deeper problems in the codebase, suggesting a need for better coding practices
info correctness gc-tuning
gcPercent = debug.SetGCPercent(gcPercent)
Adjusting garbage collection settings dynamically can be a sign of deeper problems in the codebase, suggesting a need for better coding practices
info correctness gc-tuning
debug.SetGCPercent(gcPercent)
Append on every iteration of a range loop; the final length is len(src), so make([]T, 0, len(src)) avoids the regrowth
info performance append-without-prealloc
ExtensionToLanguage[ext] = append(ExtensionToLanguage[ext], name)
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
complexityTrie.Insert(TComplexity, []byte(v))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
tokenTrie.Insert(TComplexity, []byte(v))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
tokenTrie.Insert(TComplexityPostfix, []byte(v))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
postfixExcludes = append(postfixExcludes, []byte(v))
Append on every iteration of a range loop; the final length is len(src), so make([]T, 0, len(src)) avoids the regrowth
info performance append-without-prealloc
postfixExcludes = append(postfixExcludes, []byte(v))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
slCommentTrie.Insert(TSlcomment, []byte(v))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
tokenTrie.Insert(TSlcomment, []byte(v))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
mlCommentTrie.InsertClose(TMlcomment, []byte(v[0]), []byte(v[1]))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
tokenTrie.InsertClose(TMlcomment, []byte(v[0]), []byte(v[1]))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
stringTrie.InsertClose(TString, []byte(v.Start), []byte(v.End))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
tokenTrie.InsertClose(TString, []byte(v.Start), []byte(v.End))
Append on every iteration of a range loop; the final length is len(src), so make([]T, 0, len(src)) avoids the regrowth
info performance append-without-prealloc
keywordBytes = append(keywordBytes, []byte(v))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
keywordBytes = append(keywordBytes, []byte(v))
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
literals = append(literals, []byte(l))
Unstructured output; use a structured logging library (e.g., slog, zap, zerolog, logrus)
info correctness fmt-println
fmt.Println("file or directory could not be read: " + fpath)
Unstructured output; use a structured logging library (e.g., slog, zap, zerolog, logrus)
info correctness fmt-println
fmt.Println("results written to " + FileOutput)

Get this view in your editor

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

Generated by searchcode.com · powered by code_get_file + code_get_findings