174- [gocloc](https://github.com/hhatto/gocloc) a sloc counter in Go inspired by tokei
175- [loc](https://github.com/cgag/loc) rust implementation similar to tokei but often faster
176▶- [loccount](https://gitlab.com/esr/loccount) Go implementation written and maintained by ESR
177- [polyglot](https://github.com/vmchale/polyglot) ATS sloc counter
178- [tokei](https://github.com/XAMPPRocky/tokei) fast, accurate and written in rust
· · ·
179- [sloc](https://github.com/flosse/sloc) coffeescript code counter
180▶- [stto](https://github.com/mainak55512/stto) new Go code counter with a focus on performance
181
182Interesting reading about other code counting projects tokei, loc, polyglot and loccount
· · ·
271 Count a specific folder or file:
272 scc myproject/
273▶ scc main.go
274
275 Count several paths at once:
· · ·
345 -c, --no-complexity skip calculation of code complexity
346 -d, --no-duplicates remove duplicate files from stats and output
347▶ --no-fold-authors disable the name+email-domain identity folding fallback for git author reports (mailmap still applied)
348 --no-gen ignore generated files in output (implies --gen)
349 --no-gitignore disables .gitignore file logic
· · ·
481Because some languages don't have loops and instead use recursion they can have a lower complexity count. Does this mean they are less complex? Probably not, but the tool cannot see this because it does not build an AST of the code as it only scans through it.
482
483▶Generally though the complexity there is to help estimate between projects written in the same language, or for finding the most complex file in a project `scc --by-file -s complexity` which can be useful when you are estimating on how hard something is to maintain, or when looking for those files that should probably be refactored.
484
485As for how it works.
+ 17 more matches in this file
13
14
15▶def main():
16 if len(sys.argv) < 2:
17 print(f"Usage: {sys.argv[0]} benchmark_regression.json [title]")
· · ·
40 google.charts.setOnLoadCallback(drawChart);
41
42▶ function drawChart() {{
43 var data = google.visualization.arrayToDataTable([
44 ['Version', 'Runtime (seconds)'],
· · ·
48 var options = {{
49 title: '{title}',
50▶ curveType: 'function',
51 legend: {{ position: 'bottom' }}
52 }};
· · ·
66
67
68▶if __name__ == "__main__":
69 main()
70
· · ·
1▶package main
2
3import (
· · ·
38)
39
40▶func intPtr(i int) *int {
41 return &i
42}
· · ·
43
44▶func timePtr(t time.Duration) *time.Duration {
45 return &t
46}
· · ·
47
48▶func main() {
49 http.HandleFunc("/health-check/", func(w http.ResponseWriter, r *http.Request) {
50 locationLogMutex.Lock()
· · ·
49▶ http.HandleFunc("/health-check/", func(w http.ResponseWriter, r *http.Request) {
50 locationLogMutex.Lock()
51 for k, v := range locationTracker {
+ 20 more matches in this file
1▶package main
2
3import (
· · ·
7)
8
9▶func Test_resolveColor(t *testing.T) {
10 tests := []struct {
11 name string
· · ·
55
56 for _, tt := range tests {
57▶ t.Run(tt.name, func(t *testing.T) {
58 if got := resolveColor(tt.color); got != tt.want {
59 t.Errorf("resolveColor(%q) = %q, want %q", tt.color, got, tt.want)
· · ·
63}
64
65▶func Test_formatCount(t *testing.T) {
66 type args struct {
67 count float64
· · ·
123 }
124 for _, tt := range tests {
125▶ t.Run(tt.name, func(t *testing.T) {
126 if got := formatCount(tt.args.count); got != tt.want {
127 t.Errorf("formatCount() = %v, want %v", got, tt.want)
+ 4 more matches in this file
1// SPDX-License-Identifier: MIT
2
3▶package main
4
5import (
· · ·
16)
17
18▶func printShellCompletion(cmd *cobra.Command, command string) error {
19 switch command {
20 case "bash":
· · ·
31}
32
33▶func printFlagSuggestion(flagSet *pflag.FlagSet, unknownFlag string) {
34 flags := processor.GetMostSimilarFlags(flagSet, unknownFlag)
35 if len(flags) == 0 {
· · ·
49
50//go:generate go run scripts/include.go
51▶func main() {
52 // f, _ := os.Create("scc.pprof")
53 // pprof.StartCPUProfile(f)
· · ·
86 Count a specific folder or file:
87 scc myproject/
88▶ scc main.go
89
90 Count several paths at once:
+ 2 more matches in this file
1▶package main
2
3import (
· · ·
14)
15
16▶const sccTestFlag string = "-test.main"
17
18var sccBinPath = os.Args[0]
· · ·
19
20▶func TestMain(m *testing.M) {
21 idx := slices.Index(os.Args, sccTestFlag)
22 if idx != -1 {
· · ·
23 os.Args = slices.Delete(os.Args, idx, idx+1)
24▶ main()
25 return
26 }
· · ·
29}
30
31▶func runSCC(args ...string) (string, error) {
32 args = slices.Insert(args, 0, sccTestFlag)
33 cmd := exec.Command(sccBinPath, args...)
+ 43 more matches in this file
1// SPDX-License-Identifier: MIT
2
3▶package main
4
5import (
· · ·
24var mcpMu sync.Mutex
25
26▶func startMCPServer() {
27 mcpServer := server.NewMCPServer(
28 "scc",
· · ·
149}
150
151▶func mcpAnalyzeHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
152 args := request.GetArguments()
153
· · ·
352}
353
354▶func jsonMarshal(v any) ([]byte, error) {
355 return json.MarshalIndent(v, "", " ")
356}
· · ·
358// sortFileJobs sorts a slice of FileJob pointers using the current
359// processor.SortBy value so that the most relevant files come first.
360▶func sortFileJobs(files []*processor.FileJob) {
361 switch processor.SortBy {
362 case "name", "names", "language", "languages", "lang", "langs":
+ 9 more matches in this file
75#Install-ChocolateyInstallPackage @packageArgs # https://chocolatey.org/docs/helpers-install-chocolatey-install-package
76
77▶## Main helper functions - these have error handling tucked into them already
78## see https://chocolatey.org/docs/helpers-reference
79
· · ·
93## see the full list at https://chocolatey.org/docs/helpers-reference
94
95▶## downloader that the main helpers use to download items
96## if removing $url64, please remove from here
97## - https://chocolatey.org/docs/helpers-get-chocolatey-web-file
65 Write-Warning "$($key.Count) matches found!"
66 Write-Warning "To prevent accidental data loss, no programs will be uninstalled."
67▶ Write-Warning "Please alert package maintainer the following keys were matched:"
68 $key | % {Write-Warning "- $($_.DisplayName)"}
69}
· · ·
70
71▶## OTHER POWERSHELL FUNCTIONS
72## https://chocolatey.org/docs/helpers-reference
73#Uninstall-ChocolateyZipPackage $packageName # Only necessary if you did not unpack to package directory - see https://chocolatey.org/docs/helpers-uninstall-chocolatey-zip-package
11)
12
13▶func TestGetExtension(t *testing.T) {
14 got := getExtension("something.c")
15 expected := "c"
· · ·
20}
21
22▶func TestGetExtensionNoExtension(t *testing.T) {
23 got := getExtension("something")
24 expected := "something"
· · ·
29}
30
31▶func TestGetExtensionMultipleDots(t *testing.T) {
32 got := getExtension(".travis.yml")
33 expected := "travis.yml"
· · ·
38}
39
40▶func TestGetExtensionMultipleExtensions(t *testing.T) {
41 got := getExtension("something.go.yml")
42 expected := "go.yml"
· · ·
47}
48
49▶func TestGetExtensionStartsWith(t *testing.T) {
50 got := getExtension(".gitignore")
51 expected := ".gitignore"
+ 23 more matches in this file
20
21// HistoryDepth is the maximum number of commits the history engine walks. 0
22▶// means "entire history". Wired to --depth in main.go.
23var HistoryDepth = 1000
24
· · ·
141// pass user input unchecked. A degenerate window (from == to or to before
142// from) yields Width=0; all commits land in bucket 0 / N-1.
143▶func NewBucketing(from, to time.Time, n int) Bucketing {
144 if n <= 0 {
145 n = 1
· · ·
155// clamp to 0 (defensive — should not happen given the walk window). Times at
156// or after To clamp to N-1.
157▶func (b Bucketing) Index(t time.Time) int {
158 if b.N <= 0 {
159 return 0
· · ·
180// Start returns the wall-clock start time of bucket i. Indexes outside
181// [0, N) are clamped.
182▶func (b Bucketing) Start(i int) time.Time {
183 if b.N <= 0 {
184 return b.From
· · ·
194
195// emptySnapshot is what observers see when HEAD is missing or empty.
196▶func emptySnapshot() HeadSnapshot {
197 return HeadSnapshot{Files: map[string]HeadFile{}}
198}
+ 26 more matches in this file
29// snapshots. Used to seed historyAuthorTimelineObserver with known author /
30// timestamp distributions.
31▶func makeTimelineRepo(t *testing.T, commits []timelineCommit) string {
32 t.Helper()
33 ProcessConstants()
· · ·
70}
71
72▶func findTimelineRow(t *testing.T, rows []authorTimelineRow, name string) authorTimelineRow {
73 t.Helper()
74 for _, r := range rows {
· · ·
81}
82
83▶func TestBucketingIndex(t *testing.T) {
84 from := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
85 to := time.Date(2025, 1, 11, 0, 0, 0, 0, time.UTC) // 10-day span
· · ·
113}
114
115▶func TestBucketingDegenerateWindow(t *testing.T) {
116 when := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
117 b := NewBucketing(when, when, 8)
· · ·
124}
125
126▶func TestBucketingStart(t *testing.T) {
127 from := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
128 to := from.Add(10 * 24 * time.Hour)
+ 34 more matches in this file
18
19// authorsTopN is the cap on tabular rows for the author rollup. CSV/JSON
20▶// output is not capped. The remainder collapses into a single "others (N)"
21// row in the tabular table.
22const authorsTopN = 15
· · ·
67}
68
69▶func newHistoryAuthorsObserver() *historyAuthorsObserver {
70 return &historyAuthorsObserver{
71 blame: map[string][]authorID{},
· · ·
80// baseline snapshot — every pre-window line maps to sentinelAuthorID so
81// surviving untouched lines are correctly attributed to "(before window)".
82▶func (o *historyAuthorsObserver) Seed(baseline BaselineSnapshot) {
83 o.registry = newAuthorRegistry(baseline.Mailmap)
84 for path, bf := range baseline.Files {
· · ·
93}
94
95▶func (o *historyAuthorsObserver) Observe(c CommitInfo, changes []FileChange) {
96 aid := o.registry.intern(c.Author, c.Email)
97 if prev, ok := o.lastSeen[aid]; !ok || c.When.After(prev) {
· · ·
121}
122
123▶func (o *historyAuthorsObserver) Finalise(window HistoryWindow, head HeadSnapshot) {
124 o.window = window
125 o.snapshot = head
+ 10 more matches in this file
28// using the caller's named author. Used to exercise per-author attribution
29// in the authors observer.
30▶func makeAuthoredRepo(t *testing.T, commits []authoredCommit) string {
31 t.Helper()
32 ProcessConstants()
· · ·
72// findAuthorRow returns the row whose canonical name matches `name`, or
73// fails the test.
74▶func findAuthorRow(t *testing.T, rows []authorRow, name string) authorRow {
75 t.Helper()
76 for _, r := range rows {
· · ·
83}
84
85▶func TestAuthorsLastToucherAttribution(t *testing.T) {
86 saveDepth := HistoryDepth
87 HistoryDepth = 100
· · ·
88▶ t.Cleanup(func() { HistoryDepth = saveDepth })
89
90 // Alice introduces 7 lines; Bob rewrites lines 4–7 (4 lines).
· · ·
91 // Net: Alice owns 3, Bob owns 4. No (before window) — full history walked.
92▶ first := "package x\nfunc A() {}\nfunc B() {}\nfunc C() {}\nfunc D() {}\nfunc E() {}\nfunc F() {}\n"
93 // 7 lines: line1=package, line2=A, line3=B, line4=C, line5=D, line6=E, line7=F.
94 // We want Bob to rewrite lines 4-7 (4 lines).
+ 48 more matches in this file
30// name+email so two commit identities mapped to the same canonical pair
31// collapse to one authorID. When fold is true, a second index folds
32▶// distinct emails that share the same (lowercase name, email domain) — a
33// best-effort fallback for repos without a .mailmap.
34type authorRegistry struct {
· · ·
35 nameToID map[string]authorID
36▶ byNameDomain map[string]authorID
37 records []authorRecord
38 mm *mailmap
· · ·
40}
41
42▶func newAuthorRegistry(mm *mailmap) *authorRegistry {
43 return newAuthorRegistryWithFold(mm, FoldAuthors)
44}
· · ·
45
46▶func newAuthorRegistryWithFold(mm *mailmap, fold bool) *authorRegistry {
47 return &authorRegistry{
48 nameToID: map[string]authorID{},
· · ·
49▶ byNameDomain: map[string]authorID{},
50 records: []authorRecord{{}}, // slot 0 = sentinelAuthorID
51 mm: mm,
+ 16 more matches in this file
8)
9
10▶func TestApplyDiffToBlameNewFile(t *testing.T) {
11 got := applyDiffToBlame(nil, 3, []LineRange{{Start: 1, Count: 3}}, nil, 7)
12 want := []authorID{7, 7, 7}
· · ·
16}
17
18▶func TestApplyDiffToBlameAppend(t *testing.T) {
19 prev := []authorID{1, 1}
20 got := applyDiffToBlame(prev, 4, []LineRange{{Start: 3, Count: 2}}, nil, 9)
· · ·
25}
26
27▶func TestApplyDiffToBlamePureDelete(t *testing.T) {
28 prev := []authorID{1, 1, 1, 1}
29 got := applyDiffToBlame(prev, 2, nil, []LineRange{{Start: 2, Count: 2}}, 9)
· · ·
34}
35
36▶func TestApplyDiffToBlameReplaceMiddle(t *testing.T) {
37 // 5 lines from author 1; replace line 3 with two lines from author 2.
38 prev := []authorID{1, 1, 1, 1, 1}
· · ·
47}
48
49▶func TestApplyDiffToBlamePadsToNewLines(t *testing.T) {
50 // Diff arithmetic disagrees with newLines — defensive pad with sentinel.
51 got := applyDiffToBlame(nil, 4, nil, nil, 9)
+ 29 more matches in this file
24}
25
26▶func (h *historyIgnore) Match(p string, isDir bool) bool {
27 if h == nil || h.matcher == nil {
28 return false
· · ·
35// parses them, and produces a matcher. Respects the existing --no-ignore
36// (Ignore) and --no-scc-ignore (SccIgnore) flag globals.
37▶func buildHistoryIgnore(repo *git.Repository, head plumbing.Hash) (*historyIgnore, error) {
38 commit, err := repo.CommitObject(head)
39 if err != nil {
· · ·
46
47 var patterns []gitignore.Pattern
48▶ err = tree.Files().ForEach(func(f *object.File) error {
49 if f.Mode == filemode.Dir || f.Mode == filemode.Submodule || f.Mode == filemode.Symlink {
50 return nil
· · ·
65 defer reader.Close()
66
67▶ domain := splitDomain(path.Dir(f.Name))
68 patterns = append(patterns, parseIgnoreFile(reader, domain)...)
69 return nil
· · ·
68▶ patterns = append(patterns, parseIgnoreFile(reader, domain)...)
69 return nil
70 })
+ 3 more matches in this file
10)
11
12▶func TestParseIgnoreFileSkipsCommentsAndBlanks(t *testing.T) {
13 body := strings.NewReader("# comment\n\nfoo\n!bar\n \n")
14 pats := parseIgnoreFile(body, nil)
· · ·
18}
19
20▶func TestHistoryIgnoreMatchesPattern(t *testing.T) {
21 pat := gitignore.ParsePattern("vendor/", nil)
22 h := &historyIgnore{matcher: gitignore.NewMatcher([]gitignore.Pattern{pat})}
· · ·
30}
31
32▶func TestHistoryIgnoreNilSafe(t *testing.T) {
33 var h *historyIgnore
34 if h.Match("anything", false) {
· · ·
37}
38
39▶func TestSplitDomain(t *testing.T) {
40 cases := []struct {
41 in string
· · ·
48 }
49 for _, c := range cases {
50▶ got := splitDomain(c.in)
51 if len(got) != c.want {
52 t.Errorf("splitDomain(%q) = %v, want length %d", c.in, got, c.want)
+ 1 more matches in this file
12)
13
14▶func findLanguagesRow(t *testing.T, rows []languagesTimelineRow, language string) languagesTimelineRow {
15 t.Helper()
16 for _, r := range rows {
· · ·
26// TypeScript code is added over time while JavaScript code is steadily
27// removed, the trajectory and change sign should reflect that.
28▶func TestLanguagesTimelineTSRisesJSFalls(t *testing.T) {
29 // Set HistoryDepth so the JS-baseline commit (commit 0) sits OUTSIDE
30 // the window — the engine then seeds JavaScript with that file's lines
· · ·
33 saveDepth, saveBuckets := HistoryDepth, HistoryBuckets
34 HistoryDepth, HistoryBuckets = 9, 10
35▶ t.Cleanup(func() {
36 HistoryDepth, HistoryBuckets = saveDepth, saveBuckets
37 })
· · ·
105// of the window and is then wholly removed should show codeNow == 0 and a
106// negative change.
107▶func TestLanguagesTimelineLanguageRemoval(t *testing.T) {
108 // depth=2 → only the last 2 commits are in the window. The first
109 // commit (which establishes the JS file) sits OUTSIDE the window and
· · ·
112 saveDepth, saveBuckets := HistoryDepth, HistoryBuckets
113 HistoryDepth, HistoryBuckets = 2, 6
114▶ t.Cleanup(func() {
115 HistoryDepth, HistoryBuckets = saveDepth, saveBuckets
116 })
+ 26 more matches in this file
17// historyHeader renders the centred two-line "<break> <name> · last N
18// commits · from → to <break>" block that every tabular report uses.
19▶func historyHeader(reportName string, w HistoryWindow, wide bool) string {
20 break_ := tabularBreakFor(wide)
21 var sb strings.Builder
· · ·
27}
28
29▶func formatHeaderLine(reportName string, w HistoryWindow) string {
30 if w.Commits == 0 {
31 return reportName + " · no commits"
· · ·
39// produce, honouring --no-hborder and --ci. Centralised here so every
40// history renderer agrees with the language tables.
41▶func tabularBreakFor(wide bool) string {
42 if wide {
43 return getTabularWideBreak()
· · ·
49// Falls back to ASCII '#' when --ci is on or output is not a TTY (CSV-safe
50// callers should not use this helper).
51▶func renderBar(ratio float64, width int) string {
52 if width <= 0 {
53 return ""
· · ·
69 total := ratio * float64(width)
70 full := int(total)
71▶ remainder := total - float64(full)
72 var sb strings.Builder
73 for i := 0; i < full; i++ {
+ 7 more matches in this file
20// to detect languages from file names — same as a normal scc run — and the
21// engine itself only talks to go-git, so no shell-out happens.
22▶func makeFixtureRepo(t *testing.T, commits []map[string]string) string {
23 t.Helper()
24 ProcessConstants()
· · ·
70}
71
72▶func (c *captureObserver) Observe(info CommitInfo, changes []FileChange) {
73 c.commits = append(c.commits, info)
74 c.changes = append(c.changes, changes)
· · ·
75}
76
77▶func (c *captureObserver) Finalise(w HistoryWindow, s HeadSnapshot) {
78 c.window = w
79 c.snapshot = s
· · ·
80}
81
82▶func TestRunHistoryWalksOldestFirstAndCollectsChanges(t *testing.T) {
83 // Set depth/flags to defaults this test cares about.
84 saveDepth := HistoryDepth
· · ·
85 HistoryDepth = 100
86▶ t.Cleanup(func() { HistoryDepth = saveDepth })
87
88 dir := makeFixtureRepo(t, []map[string]string{
+ 20 more matches in this file
8)
9
10▶func TestLocomoComplexityDensityZeroCode(t *testing.T) {
11 got := LocomoComplexityDensity(10, 0)
12 if got != 0 {
· · ·
15}
16
17▶func TestLocomoComplexityDensity(t *testing.T) {
18 got := LocomoComplexityDensity(30, 100)
19 if math.Abs(got-0.3) > 0.001 {
· · ·
22}
23
24▶func TestLocomoComplexityFactor(t *testing.T) {
25 // density 0.3, weight 5 → 1 + sqrt(0.3)*5 ≈ 1 + 0.5477*5 ≈ 3.738
26 got := LocomoComplexityFactor(0.3, 5)
· · ·
30}
31
32▶func TestLocomoComplexityFactorLowDensity(t *testing.T) {
33 // density 0.05, weight 5 → 1 + sqrt(0.05)*5 ≈ 1 + 0.2236*5 ≈ 2.118
34 got := LocomoComplexityFactor(0.05, 5)
· · ·
38}
39
40▶func TestLocomoIterationFactor(t *testing.T) {
41 // density 0.3, base 1.5, weight 2 → 1.5 + sqrt(0.3)*2 ≈ 1.5 + 1.095 ≈ 2.595
42 got := LocomoIterationFactor(0.3, 1.5, 2)
+ 13 more matches in this file
161}
162
163▶func parseRemapRules(value string) []remapRule {
164 rules := []remapRule{}
165
· · ·
177}
178
179▶func newRemapConfig(remapAll string, remapUnknown string) remapConfig {
180 return remapConfig{
181 all: parseRemapRules(remapAll),
· · ·
339
340// HistoryBuckets is the time-bucket resolution for the timeline reports.
341▶// Wired to --buckets in main.go; default 60.
342var HistoryBuckets = 60
343
· · ·
344▶// FoldAuthors enables the name+domain identity folding fallback applied
345// after the mailmap. Toggled off via --no-fold-authors.
346var FoldAuthors = true
· · ·
370// ConfigureGc needs to be set outside of ProcessConstants because it should only be enabled in command line
371// mode https://github.com/boyter/scc/issues/32
372▶func ConfigureGc() {
373 gcPercent = debug.SetGCPercent(gcPercent)
374}
+ 20 more matches in this file
20
21// DefaultReportName is the file name used when --report is invoked without
22▶// a path (pflag's NoOptDefVal). main.go wires this in as the bare-flag
23// default; runReport compares ReportOut to it to decide whether the user
24// supplied an explicit path or relied on the default.
· · ·
35
36// ReportSkipNames is the parsed, lower-cased set of section names supplied
37▶// via --report-skip. Wired from main.go (spec 05). CollectReportData reads
38// this through ReportSkipped to decide which *Result pointers to nil out
39// before returning.
· · ·
63// --report-skip. Section names are case-insensitive — callers can pass
64// either case.
65▶func ReportSkipped(section string) bool {
66 if len(ReportSkipNames) == 0 {
67 return false
· · ·
244
245 // Rendered share-card SVG (data: URL safe). Populated by RenderReport
246▶ // before the main template runs so it can be embedded as og:image.
247 CardSVG template.HTML
248}
· · ·
261}
262
263▶func saveReportFlags() reportFlagState {
264 return reportFlagState{
265 UlocMode: UlocMode,
+ 21 more matches in this file
34// first use so a normal scc invocation pays nothing for the report path. The
35// returned root template has both "report" and "card" defined.
36▶func reportTemplate() *template.Template {
37 reportTmplOnce.Do(func() {
38 root := template.New("report").Funcs(reportFuncs)
· · ·
37▶ reportTmplOnce.Do(func() {
38 root := template.New("report").Funcs(reportFuncs)
39 reportTmpl = template.Must(root.Parse(reportTemplateSrc))
· · ·
38▶ root := template.New("report").Funcs(reportFuncs)
39 reportTmpl = template.Must(root.Parse(reportTemplateSrc))
40 template.Must(reportTmpl.New("card").Parse(reportCardSrc))
· · ·
49// that future template helpers can surface "you asked for X but it's not a
50// real section" hints without re-parsing.
51▶func parseReportSkip(raw string) {
52 parseReportSkipTo(raw, os.Stderr)
53}
· · ·
56// destination is plumbed through so unit tests can capture stderr output
57// without resorting to os.Stderr redirection.
58▶func parseReportSkipTo(raw string, warnW io.Writer) {
59 ReportSkipNames = map[string]bool{}
60 if strings.TrimSpace(raw) == "" {
+ 47 more matches in this file