config_test.go GO 832 lines View on github.com → Search inside
1// SPDX-License-Identifier: MIT23package main45import (6	"os"7	"os/exec"8	"path/filepath"9	"slices"10	"strings"11	"testing"1213	"github.com/boyter/scc/v3/processor"14	"github.com/spf13/pflag"15)1617// noGlobalConfig forces SCC_CONFIG_PATH empty (treated as unset) so a value in18// the developer's environment cannot pollute config tests. Pass it as the env19// argument to runSCCDir for any test that does not set its own global config.20var noGlobalConfig = []string{SccConfigEnv + "="}2122// runSCCDir runs the test binary as scc in the given working directory, with23// optional extra environment entries (KEY=VALUE).24func runSCCDir(t *testing.T, dir string, env []string, args ...string) (string, error) {25	t.Helper()26	bin, err := filepath.Abs(sccBinPath)27	if err != nil {28		t.Fatal(err)29	}30	full := slices.Insert(slices.Clone(args), 0, sccTestFlag)31	cmd := exec.Command(bin, full...)32	cmd.Dir = dir33	if len(env) > 0 {34		cmd.Env = append(os.Environ(), env...)35	}36	out, err := cmd.CombinedOutput()37	return string(out), err38}3940func TestParseConfigArgs(t *testing.T) {41	t.Parallel()42	tests := []struct {43		name            string44		content         string45		allowPositional bool46		want            []string47	}{48		{49			name:    "single flag per line",50			content: "--no-cocomo\n--by-file\n",51			want:    []string{"--no-cocomo", "--by-file"},52		},53		{54			name:    "multiple tokens per line",55			content: "--exclude-dir vendor\n",56			want:    []string{"--exclude-dir", "vendor"},57		},58		{59			name:    "single quotes group a value",60			content: "--count-as 'jsp:html'\n",61			want:    []string{"--count-as", "jsp:html"},62		},63		{64			name:    "double quotes allow embedded single quote",65			content: "--count-as \"a'b\"\n",66			want:    []string{"--count-as", "a'b"},67		},68		{69			name:    "whole-line comment skipped",70			content: "# a comment\n--by-file\n",71			want:    []string{"--by-file"},72		},73		{74			name:    "inline trailing comment stripped",75			content: "--format wide   # comment\n",76			want:    []string{"--format", "wide"},77		},78		{79			name:    "blank lines produce no tokens",80			content: "\n\n--by-file\n\n   \n",81			want:    []string{"--by-file"},82		},83		{84			name:    "backslash is literal (windows path)",85			content: "--exclude-dir C:\\build\\out\n",86			want:    []string{"--exclude-dir", "C:\\build\\out"},87		},88		{89			name:    "crlf line endings",90			content: "--no-cocomo\r\n--by-file\r\n",91			want:    []string{"--no-cocomo", "--by-file"},92		},93		{94			name:            "positional line dropped when not allowed",95			content:         "src/\n--by-file\n",96			allowPositional: false,97			want:            []string{"--by-file"},98		},99		{100			name:            "positional line kept when allowed",101			content:         "src/\n--by-file\n",102			allowPositional: true,103			want:            []string{"src/", "--by-file"},104		},105		{106			name:    "empty quoted value still a token",107			content: "--report-title \"\"\n",108			want:    []string{"--report-title", ""},109		},110		{111			name:            "end-of-flags marker dropped in config",112			content:         "--\n--by-file\n",113			allowPositional: false,114			want:            []string{"--by-file"},115		},116		{117			name:            "end-of-flags marker dropped mid-line in config",118			content:         "--exclude-dir vendor --\n",119			allowPositional: false,120			want:            []string{"--exclude-dir", "vendor"},121		},122		{123			name:            "end-of-flags marker kept in @file",124			content:         "--\nsrc/\n",125			allowPositional: true,126			want:            []string{"--", "src/"},127		},128	}129130	for _, tc := range tests {131		t.Run(tc.name, func(t *testing.T) {132			got := parseConfigArgs(tc.content, tc.allowPositional)133			if !slices.Equal(got, tc.want) {134				t.Errorf("parseConfigArgs(%q, %v) = %q, want %q", tc.content, tc.allowPositional, got, tc.want)135			}136		})137	}138}139140func TestPreScanConfig(t *testing.T) {141	t.Parallel()142	tests := []struct {143		name         string144		args         []string145		wantNoConfig bool146		wantFindRoot bool147		wantPath     string148	}{149		{name: "nothing", args: []string{"scc", "."}},150		{name: "no-config", args: []string{"scc", "--no-config"}, wantNoConfig: true},151		{name: "long find-root-config", args: []string{"scc", "--find-root-config"}, wantFindRoot: true},152		{name: "no -r shorthand", args: []string{"scc", "-r"}},153		{name: "-r no longer clusters into find-root", args: []string{"scc", "-rv"}},154		{name: "non-r cluster", args: []string{"scc", "-vd"}},155		{name: "config space form", args: []string{"scc", "--config", "team.sccconfig"}, wantPath: "team.sccconfig"},156		{name: "config equals form", args: []string{"scc", "--config=team.sccconfig"}, wantPath: "team.sccconfig"},157		{name: "config as final token (typo)", args: []string{"scc", "--config"}},158		{name: "all three", args: []string{"scc", "--no-config", "--find-root-config", "--config=x"}, wantNoConfig: true, wantFindRoot: true, wantPath: "x"},159	}160161	for _, tc := range tests {162		t.Run(tc.name, func(t *testing.T) {163			noConfig, findRoot, path := preScanConfig(tc.args)164			if noConfig != tc.wantNoConfig || findRoot != tc.wantFindRoot || path != tc.wantPath {165				t.Errorf("preScanConfig(%q) = (%v,%v,%q), want (%v,%v,%q)",166					tc.args, noConfig, findRoot, path, tc.wantNoConfig, tc.wantFindRoot, tc.wantPath)167			}168		})169	}170}171172func TestMergeSliceDefault(t *testing.T) {173	t.Parallel()174	defaults := []string{".git", ".hg", ".svn"}175176	if got := mergeSliceDefault(nil, defaults); !slices.Equal(got, defaults) {177		t.Errorf("empty set should fall back to defaults, got %q", got)178	}179	if got := mergeSliceDefault([]string{"vendor"}, defaults); !slices.Equal(got, []string{".git", ".hg", ".svn", "vendor"}) {180		t.Errorf("defaults should be preserved, got %q", got)181	}182	if got := mergeSliceDefault([]string{"vendor", "dist"}, defaults); !slices.Equal(got, []string{".git", ".hg", ".svn", "vendor", "dist"}) {183		t.Errorf("union should be order-stable, got %q", got)184	}185	if got := mergeSliceDefault([]string{".git", "vendor"}, defaults); !slices.Equal(got, []string{".git", ".hg", ".svn", "vendor"}) {186		t.Errorf("duplicate should be removed, got %q", got)187	}188}189190// TestRegisterFlagsExhaustive asserts every flag registerFlags is meant to191// register is present in the resulting flag set. The write-only mode must192// give every non-write flag a sink; a missing flag there is silent state193// corruption, so this check is the insurance.194func TestRegisterFlagsExhaustive(t *testing.T) {195	t.Parallel()196	fs := pflag.NewFlagSet("test", pflag.ContinueOnError)197	var out, report, multi string198	registerFlags(fs, &flagBindings{output: &out, report: &report, formatMulti: &multi, inert: true})199200	expected := []string{201		"character", "percent", "uloc", "dryness", "cognitive", "binary", "by-file", "ci",202		"no-ignore", "no-scc-ignore", "no-gitignore", "no-gitmodule", "count-ignore",203		"ignore-file",204		"debug", "exclude-dir", "file-gc-count", "file-list-queue-size",205		"file-process-job-workers", "file-summary-job-queue-size",206		"directory-walker-job-workers", "format", "report", "report-skip",207		"report-title", "include-ext", "exclude-ext", "exclude-file", "languages",208		"avg-wage", "overhead", "eaf", "sloccount-format", "no-cocomo",209		"cocomo-project-type", "no-size", "no-hborder", "size-unit", "no-complexity",210		"no-duplicates", "min-gen", "min", "gen", "generated-markers", "no-min-gen",211		"no-min", "no-gen", "min-gen-line-length", "not-match", "output", "sort",212		"trace", "verbose", "wide", "no-large", "include-symlinks", "large-line-count",213		"large-byte-count", "count-as", "count-as-pattern", "format-multi",214		"sql-project", "remap-unknown", "remap-all", "currency-symbol", "locomo",215		"cost-comparison", "locomo-preset", "locomo-review", "locomo-config",216		"locomo-input-price", "locomo-output-price", "locomo-tps", "locomo-cycles",217		"hotspots", "by-author", "depth", "timeline", "buckets", "no-fold-authors",218		"mcp",219	}220	for _, name := range expected {221		if fs.Lookup(name) == nil {222			t.Errorf("registerFlags did not register --%s", name)223		}224	}225}226227func TestRegisterFlagsSliceDefValue(t *testing.T) {228	t.Parallel()229	fs := pflag.NewFlagSet("test", pflag.ContinueOnError)230	var out, report, multi string231	registerFlags(fs, &flagBindings{output: &out, report: &report, formatMulti: &multi, inert: true})232233	cases := map[string]string{234		"exclude-dir":       "[.git,.hg,.svn]",235		"exclude-file":      "[package-lock.json,Cargo.lock,yarn.lock,pubspec.lock,Podfile.lock,pnpm-lock.yaml]",236		"generated-markers": "[do not edit,<auto-generated />]",237	}238	for name, want := range cases {239		f := fs.Lookup(name)240		if f == nil {241			t.Fatalf("missing flag --%s", name)242		}243		if f.DefValue != want {244			t.Errorf("--%s DefValue = %q, want %q", name, f.DefValue, want)245		}246	}247}248249// TestCognitiveFlagParses asserts that parsing --cognitive on the real flag set250// toggles the processor.Cognitive global.251func TestCognitiveFlagParses(t *testing.T) {252	prev := processor.Cognitive253	defer func() { processor.Cognitive = prev }()254	processor.Cognitive = false255256	fs := pflag.NewFlagSet("test", pflag.ContinueOnError)257	var out, report, multi string258	registerFlags(fs, &flagBindings{output: &out, report: &report, formatMulti: &multi})259260	if err := fs.Parse([]string{"--cognitive"}); err != nil {261		t.Fatalf("parse --cognitive: %v", err)262	}263	if !processor.Cognitive {264		t.Error("parsing --cognitive did not set processor.Cognitive")265	}266}267268// TestCognitiveFlagConfigRoundTrip writes a .sccconfig containing --cognitive,269// parses it back through the same config-arg reader and flag set that scc uses270// at startup, and asserts the flag survives the round-trip. Mirrors how a bool271// flag reaches the real global from a project config file.272func TestCognitiveFlagConfigRoundTrip(t *testing.T) {273	prev := processor.Cognitive274	defer func() { processor.Cognitive = prev }()275	processor.Cognitive = false276277	args := parseConfigArgs("--cognitive\n", false)278	if !slices.Equal(args, []string{"--cognitive"}) {279		t.Fatalf("config parse produced %q, want [--cognitive]", args)280	}281282	fs := pflag.NewFlagSet("test", pflag.ContinueOnError)283	var out, report, multi string284	registerFlags(fs, &flagBindings{output: &out, report: &report, formatMulti: &multi})285	if err := fs.Parse(args); err != nil {286		t.Fatalf("parse config args: %v", err)287	}288	if !processor.Cognitive {289		t.Error("--cognitive from config did not set processor.Cognitive")290	}291}292293// writeSccConfig creates a temp dir with a .sccconfig file and returns the dir.294func writeSccConfig(t *testing.T, content string) string {295	t.Helper()296	dir := t.TempDir()297	if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte(content), 0644); err != nil {298		t.Fatal(err)299	}300	// a source file so output is meaningful301	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nfunc main() {}\n"), 0644); err != nil {302		t.Fatal(err)303	}304	return dir305}306307func TestConfigProjectLoaded(t *testing.T) {308	dir := writeSccConfig(t, "--format csv\n--no-cocomo\n")309	out, err := runSCCDir(t, dir, noGlobalConfig)310	if err != nil {311		t.Fatal(err)312	}313	if !strings.Contains(out, "Language,Lines,Code") {314		t.Errorf("project .sccconfig --format csv not applied, output:\n%s", out)315	}316	if strings.Contains(out, "Estimated Cost to Develop") {317		t.Errorf("project .sccconfig --no-cocomo not applied, output:\n%s", out)318	}319}320321func TestConfigNoWalkUp(t *testing.T) {322	dir := writeSccConfig(t, "--format csv\n")323	sub := filepath.Join(dir, "sub")324	if err := os.Mkdir(sub, 0755); err != nil {325		t.Fatal(err)326	}327	if err := os.WriteFile(filepath.Join(sub, "x.go"), []byte("package x\n"), 0644); err != nil {328		t.Fatal(err)329	}330	out, err := runSCCDir(t, sub, noGlobalConfig)331	if err != nil {332		t.Fatal(err)333	}334	if strings.Contains(out, "Language,Lines,Code") {335		t.Errorf("config should NOT be picked up from a subdirectory (no walk-up), output:\n%s", out)336	}337}338339func TestConfigPrecedenceCLIWins(t *testing.T) {340	dir := writeSccConfig(t, "--format csv\n")341	out, err := runSCCDir(t, dir, noGlobalConfig, "--format", "json")342	if err != nil {343		t.Fatal(err)344	}345	if !strings.HasPrefix(strings.TrimSpace(out), "[{") {346		t.Errorf("CLI --format json should override config csv, output:\n%s", out)347	}348}349350func TestConfigNeverWritesFile(t *testing.T) {351	cases := []string{352		"-o evil.csv\n",353		"--output evil.csv\n",354		"-do evil.csv\n", // clustered: -d + -o (headline case)355		"-do/evil.csv\n", // clustered, no space356		"--report=evil.csv\n",357		"--format-multi tabular:stdout,csv:evil.csv\n",358	}359	for _, content := range cases {360		dir := writeSccConfig(t, content)361		_, err := runSCCDir(t, dir, noGlobalConfig)362		if err != nil {363			t.Fatalf("scc errored for config %q: %v", content, err)364		}365		if _, statErr := os.Stat(filepath.Join(dir, "evil.csv")); statErr == nil {366			t.Errorf("config %q caused a file write (evil.csv exists)", content)367		}368	}369}370371func TestConfigPresentCLICanWrite(t *testing.T) {372	dir := writeSccConfig(t, "--no-cocomo\n")373	target := filepath.Join(dir, "good.csv")374	_, err := runSCCDir(t, dir, noGlobalConfig, "-f", "csv", "-o", target)375	if err != nil {376		t.Fatal(err)377	}378	if info, statErr := os.Stat(target); statErr != nil || info.Size() == 0 {379		t.Errorf("genuine CLI -o should write the file even with config present")380	}381}382383func TestConfigControlFlagWithWriteFlag(t *testing.T) {384	// scc --config x.sccconfig -o out.csv: the CLI-only parse must accept --config so385	// -o still writes.386	dir := t.TempDir()387	cfg := filepath.Join(dir, "team.sccconfig")388	if err := os.WriteFile(cfg, []byte("--no-cocomo\n"), 0644); err != nil {389		t.Fatal(err)390	}391	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {392		t.Fatal(err)393	}394	target := filepath.Join(dir, "out.csv")395	_, err := runSCCDir(t, dir, noGlobalConfig, "--config", cfg, "-f", "csv", "-o", target)396	if err != nil {397		t.Fatal(err)398	}399	if info, statErr := os.Stat(target); statErr != nil || info.Size() == 0 {400		t.Errorf("scc --config x.sccconfig -o out.csv should still write out.csv")401	}402}403404// TestConfigDashDashDoesNotDisableCLIFlags guards against a '--' end-of-flags405// marker in a config file. Config tokens are prepended, so an unstripped '--'406// would terminate flag parsing and turn the genuine CLI's flags into positional407// paths (here --by-file would be read as a file path).408func TestConfigDashDashDoesNotDisableCLIFlags(t *testing.T) {409	dir := writeSccConfig(t, "--\n")410	out, err := runSCCDir(t, dir, noGlobalConfig, "--by-file")411	if err != nil {412		t.Fatalf("scc errored: %v\n%s", err, out)413	}414	if strings.Contains(out, "could not be read") {415		t.Errorf("--by-file was treated as a path; '--' from config leaked through:\n%s", out)416	}417}418419// TestConfigExplicitEmptyWriteFlagNoFalseWarn checks that an explicit empty420// write flag on the CLI (--output=) is recognised as "set on the CLI" and does421// not trigger the "ignoring --output from config" warning, while a config-only422// write still does warn.423func TestConfigExplicitEmptyWriteFlagNoFalseWarn(t *testing.T) {424	dir := writeSccConfig(t, "--output evil.csv\n")425	const warn = "ignoring --output from config"426427	// CLI explicitly sets --output= (empty -> stdout): the flag WAS set on the428	// command line, so no warning should fire.429	out, err := runSCCDir(t, dir, noGlobalConfig, "--output=")430	if err != nil {431		t.Fatalf("scc errored: %v\n%s", err, out)432	}433	if strings.Contains(out, warn) {434		t.Errorf("explicit --output= on CLI should not warn about config:\n%s", out)435	}436437	// Control: config-only write (no CLI override) should still warn.438	out, err = runSCCDir(t, dir, noGlobalConfig)439	if err != nil {440		t.Fatalf("scc errored: %v\n%s", err, out)441	}442	if !strings.Contains(out, warn) {443		t.Errorf("config-only --output should still warn, got:\n%s", out)444	}445}446447func TestConfigEnvVarGlobal(t *testing.T) {448	dir := t.TempDir()449	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {450		t.Fatal(err)451	}452	global := filepath.Join(dir, "global.sccconfig")453	if err := os.WriteFile(global, []byte("--format csv\n"), 0644); err != nil {454		t.Fatal(err)455	}456	out, err := runSCCDir(t, dir, []string{SccConfigEnv + "=" + global})457	if err != nil {458		t.Fatal(err)459	}460	if !strings.Contains(out, "Language,Lines,Code") {461		t.Errorf("SCC_CONFIG_PATH global not applied, output:\n%s", out)462	}463464	// set-but-empty is treated as unset (tabular default)465	out, err = runSCCDir(t, dir, []string{SccConfigEnv + "="})466	if err != nil {467		t.Fatal(err)468	}469	if strings.Contains(out, "Language,Lines,Code") {470		t.Errorf("empty SCC_CONFIG_PATH should be treated as unset, output:\n%s", out)471	}472}473474func TestConfigNoConfigComposition(t *testing.T) {475	// --config x.sccconfig --no-config loads exactly x.sccconfig and skips the project .sccconfig.476	dir := writeSccConfig(t, "--by-file\n") // project .sccconfig would set --by-file477	global := filepath.Join(dir, "iso.sccconfig")478	if err := os.WriteFile(global, []byte("--format csv\n"), 0644); err != nil {479		t.Fatal(err)480	}481	out, err := runSCCDir(t, dir, noGlobalConfig, "--config", global, "--no-config")482	if err != nil {483		t.Fatal(err)484	}485	if !strings.Contains(out, "Language,Lines,Code") {486		t.Errorf("--config should still load with --no-config, output:\n%s", out)487	}488	// --by-file from the project .sccconfig must NOT apply under --no-config: the489	// per-file CSV header carries a Location column, the summary one does not.490	if strings.Contains(out, "Location") {491		t.Errorf("--no-config should skip the project .sccconfig, output:\n%s", out)492	}493}494495func TestConfigInsideFileIsInert(t *testing.T) {496	// A --config written inside a project .sccconfig must not chain-load another file.497	dir := t.TempDir()498	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {499		t.Fatal(err)500	}501	chained := filepath.Join(dir, "chained.sccconfig")502	if err := os.WriteFile(chained, []byte("--format csv\n"), 0644); err != nil {503		t.Fatal(err)504	}505	if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--config "+chained+"\n"), 0644); err != nil {506		t.Fatal(err)507	}508	out, err := runSCCDir(t, dir, noGlobalConfig)509	if err != nil {510		t.Fatal(err)511	}512	if strings.Contains(out, "Language,Lines,Code") {513		t.Errorf("--config inside a .sccconfig must be inert (no chain-load), output:\n%s", out)514	}515}516517func TestConfigUnreadableExplicitErrors(t *testing.T) {518	dir := t.TempDir()519	out, err := runSCCDir(t, dir, noGlobalConfig, "--config", filepath.Join(dir, "does-not-exist.sccconfig"))520	if err == nil {521		t.Fatalf("scc should exit non-zero for an unreadable --config, output:\n%s", out)522	}523	if !strings.Contains(out, "could not read config") {524		t.Errorf("expected a clear error for unreadable --config, output:\n%s", out)525	}526}527528func TestConfigCoupledMinFlags(t *testing.T) {529	// config --min + CLI --no-min must resolve exactly as without the feature:530	// the CLI-only write parse must not re-fire the coupled min/gen closures.531	dir := writeSccConfig(t, "--min\n")532	// --no-min on the CLI wins (last); the run should still succeed and not crash.533	if _, err := runSCCDir(t, dir, noGlobalConfig, "--no-min"); err != nil {534		t.Fatalf("coupled min flags run failed: %v", err)535	}536}537538func TestConfigUnknownFlagAttribution(t *testing.T) {539	dir := writeSccConfig(t, "--not-a-real-flag\n")540	out, err := runSCCDir(t, dir, noGlobalConfig)541	if err == nil {542		t.Fatalf("unknown flag in config should exit non-zero, output:\n%s", out)543	}544	if !strings.Contains(out, "project") || !strings.Contains(out, "unknown flag --not-a-real-flag") {545		t.Errorf("unknown config flag should be attributed to the project file, output:\n%s", out)546	}547}548549func TestConfigCompletionUnaffected(t *testing.T) {550	// scc completion --shell bash in a dir containing ./.sccconfig must still emit551	// completions (config prepend must not shift args[1]).552	dir := writeSccConfig(t, "--format csv\n")553	out, err := runSCCDir(t, dir, noGlobalConfig, "completion", "--shell", "bash")554	if err != nil {555		t.Fatal(err)556	}557	if !strings.Contains(out, "bash completion") && !strings.Contains(out, "complete -") {558		t.Errorf("completion should still work with ./.sccconfig present, output:\n%s", out[:min(len(out), 200)])559	}560}561562func TestConfigInsideAtFileHonored(t *testing.T) {563	// --config inside an @file IS honored (asymmetry vs config files): @file564	// replaces os.Args before the pre-scan, so it is part of the genuine CLI.565	dir := t.TempDir()566	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {567		t.Fatal(err)568	}569	global := filepath.Join(dir, "from-atfile.sccconfig")570	if err := os.WriteFile(global, []byte("--format csv\n"), 0644); err != nil {571		t.Fatal(err)572	}573	atFile := filepath.Join(dir, "flags.txt")574	if err := os.WriteFile(atFile, []byte("--config "+global+"\n"), 0644); err != nil {575		t.Fatal(err)576	}577	out, err := runSCCDir(t, dir, noGlobalConfig, "@"+atFile)578	if err != nil {579		t.Fatal(err)580	}581	if !strings.Contains(out, "Language,Lines,Code") {582		t.Errorf("--config inside an @file should be honored, output:\n%s", out)583	}584}585586// TestConfigCompleteDynamicUnaffected is the regression guard for cobra's587// hidden __complete command (the one a shell calls on every TAB). Config tokens588// are prepended ahead of the genuine argv, so without the completion bypass the589// __complete subcommand is shifted out of args[0] and dynamic completion breaks590// in any directory containing a ./.sccconfig. (TestConfigCompletionUnaffected covers591// only the separate `completion --shell` script-generation path.)592func TestConfigCompleteDynamicUnaffected(t *testing.T) {593	dir := writeSccConfig(t, "--format csv\n")594	out, err := runSCCDir(t, dir, noGlobalConfig, "__complete", "--for")595	if err != nil {596		t.Fatalf("__complete errored with ./.sccconfig present: %v\n%s", err, out)597	}598	if !strings.Contains(out, "--format") {599		t.Errorf("dynamic completion broken with ./.sccconfig present, output:\n%s", out)600	}601	// A leaked config token would surface as an unknown-flag / read error.602	if strings.Contains(out, "unknown flag") || strings.Contains(out, "could not be read") {603		t.Errorf("config tokens leaked into the completion invocation, output:\n%s", out)604	}605}606607// TestConfigSliceUnion locks the §7 union semantics end-to-end: a slice flag set608// in the config file and again on the CLI must combine with each other AND with609// the built-in defaults (defaults ∪ config ∪ CLI), rather than replacing them.610func TestConfigSliceUnion(t *testing.T) {611	dir := t.TempDir()612	for _, d := range []string{"vendor", "dist", "keep", ".git"} {613		if err := os.Mkdir(filepath.Join(dir, d), 0755); err != nil {614			t.Fatal(err)615		}616	}617	for _, f := range []string{"vendor/v.go", "dist/d.go", "keep/k.go", ".git/g.go"} {618		if err := os.WriteFile(filepath.Join(dir, f), []byte("package x\n"), 0644); err != nil {619			t.Fatal(err)620		}621	}622	// config excludes vendor; CLI excludes dist; .git is a built-in default.623	if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--exclude-dir vendor\n"), 0644); err != nil {624		t.Fatal(err)625	}626627	out, err := runSCCDir(t, dir, noGlobalConfig, "--exclude-dir", "dist", "-f", "csv", "--by-file")628	if err != nil {629		t.Fatal(err)630	}631	if !strings.Contains(out, "k.go") {632		t.Errorf("keep/k.go should be counted, output:\n%s", out)633	}634	for _, gone := range []string{"v.go", "d.go", "g.go"} {635		if strings.Contains(out, gone) {636			t.Errorf("%s should be excluded (defaults ∪ config ∪ CLI), output:\n%s", gone, out)637		}638	}639}640641// TestAtFileMultiTokenAndComments locks the phase-01 @file improvements: a line642// may carry multiple whitespace-separated tokens, '#' comments are stripped and643// blank lines are dropped (the old splitter emitted one token per line and a644// stray empty arg per blank line).645func TestAtFileMultiTokenAndComments(t *testing.T) {646	dir := t.TempDir()647	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {648		t.Fatal(err)649	}650	atFile := filepath.Join(dir, "flags.txt")651	// multi-token flag line, a whole-line comment, an inline comment and a blank.652	content := "# count this project\n\n--format csv   # as CSV\n--no-cocomo\n"653	if err := os.WriteFile(atFile, []byte(content), 0644); err != nil {654		t.Fatal(err)655	}656	out, err := runSCCDir(t, dir, noGlobalConfig, "@"+atFile)657	if err != nil {658		t.Fatalf("@file errored: %v\n%s", err, out)659	}660	if !strings.Contains(out, "Language,Lines,Code") {661		t.Errorf("@file multi-token --format csv not applied, output:\n%s", out)662	}663	if strings.Contains(out, "Estimated Cost to Develop") {664		t.Errorf("@file --no-cocomo not applied, output:\n%s", out)665	}666}667668// TestAtFileQuotedSpacePath documents the @file consequence of the shared669// quote-aware tokenizer: a path containing spaces must be quoted (the old670// splitter kept a whole line as one token, so unquoted spaces used to work).671// This pins the supported workaround.672func TestAtFileQuotedSpacePath(t *testing.T) {673	dir := t.TempDir()674	if err := os.WriteFile(filepath.Join(dir, "my file.go"), []byte("package x\n"), 0644); err != nil {675		t.Fatal(err)676	}677	atFile := filepath.Join(dir, "flags.txt")678	if err := os.WriteFile(atFile, []byte("'my file.go'\n"), 0644); err != nil {679		t.Fatal(err)680	}681	out, err := runSCCDir(t, dir, noGlobalConfig, "@"+atFile)682	if err != nil {683		t.Fatalf("@file errored: %v\n%s", err, out)684	}685	if strings.Contains(out, "could not be read") {686		t.Errorf("quoted space path in @file should resolve as one token, output:\n%s", out)687	}688	if !strings.Contains(out, "Go") {689		t.Errorf("quoted 'my file.go' should be counted, output:\n%s", out)690	}691}692693// TestConfigFindRootWalkUp locks the headline --find-root-config feature694// end-to-end (§3.2): run from a subdirectory of a repo, --find-root-config must695// walk up to the repository root (detected via a .git dir) and load the root696// .sccconfig, where the default ./.sccconfig discovery finds nothing. TestConfigNoWalkUp697// already proves the default does NOT walk; this proves --find-root-config does.698func TestConfigFindRootWalkUp(t *testing.T) {699	root := t.TempDir()700	if err := os.Mkdir(filepath.Join(root, ".git"), 0755); err != nil {701		t.Fatal(err)702	}703	if err := os.WriteFile(filepath.Join(root, ".sccconfig"), []byte("--format csv\n"), 0644); err != nil {704		t.Fatal(err)705	}706	sub := filepath.Join(root, "sub")707	if err := os.Mkdir(sub, 0755); err != nil {708		t.Fatal(err)709	}710	if err := os.WriteFile(filepath.Join(sub, "x.go"), []byte("package x\n"), 0644); err != nil {711		t.Fatal(err)712	}713714	// Control: without --find-root-config, no walk-up, the root .sccconfig is not seen (tabular).715	out, err := runSCCDir(t, sub, noGlobalConfig)716	if err != nil {717		t.Fatal(err)718	}719	if strings.Contains(out, "Language,Lines,Code") {720		t.Errorf("default discovery must not walk up to the repo-root .sccconfig, output:\n%s", out)721	}722723	// --find-root-config walks up to the repo root and loads its .sccconfig (csv).724	out, err = runSCCDir(t, sub, noGlobalConfig, "--find-root-config")725	if err != nil {726		t.Fatal(err)727	}728	if !strings.Contains(out, "Language,Lines,Code") {729		t.Errorf("--find-root-config should walk up to the repo-root .sccconfig, output:\n%s", out)730	}731}732733// TestConfigFindRootOutsideRepoFallback locks the §3.2 graceful-degradation734// guarantee: when the CWD is not inside any repo, FindRepositoryRoot returns the735// supplied directory unchanged, so --find-root-config resolves to ./.sccconfig -736// identical to default discovery, never an error. --find-root-config must737// therefore always be safe to leave on.738func TestConfigFindRootOutsideRepoFallback(t *testing.T) {739	dir := writeSccConfig(t, "--format csv\n") // ./.sccconfig, no .git anywhere above740	out, err := runSCCDir(t, dir, noGlobalConfig, "--find-root-config")741	if err != nil {742		t.Fatalf("--find-root-config outside a repo should not error, got: %v\n%s", err, out)743	}744	if !strings.Contains(out, "Language,Lines,Code") {745		t.Errorf("--find-root-config outside a repo should fall back to ./.sccconfig, output:\n%s", out)746	}747}748749// TestConfigEnvVarUnreadableErrors covers the SCC_CONFIG_PATH arm of the §8750// "explicit source the user asked for" rule: a set-but-unreadable global must751// exit non-zero with a clear error. TestConfigUnreadableExplicitErrors only752// covers the --config arm; the env-var resolution is a separate branch.753func TestConfigEnvVarUnreadableErrors(t *testing.T) {754	dir := t.TempDir()755	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {756		t.Fatal(err)757	}758	missing := filepath.Join(dir, "does-not-exist.sccconfig")759	out, err := runSCCDir(t, dir, []string{SccConfigEnv + "=" + missing})760	if err == nil {761		t.Fatalf("an unreadable SCC_CONFIG_PATH should exit non-zero, output:\n%s", out)762	}763	if !strings.Contains(out, "could not read config") {764		t.Errorf("expected a clear error for an unreadable SCC_CONFIG_PATH, output:\n%s", out)765	}766}767768// TestConfigNoConfigDisablesEnvGlobal proves --no-config skips a *set*769// SCC_CONFIG_PATH global (§4). TestConfigNoConfigComposition only exercises770// --config (which is honored even under --no-config); the env-var global is the771// arm that --no-config actually disables.772func TestConfigNoConfigDisablesEnvGlobal(t *testing.T) {773	dir := t.TempDir()774	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {775		t.Fatal(err)776	}777	global := filepath.Join(dir, "global.sccconfig")778	if err := os.WriteFile(global, []byte("--format csv\n"), 0644); err != nil {779		t.Fatal(err)780	}781782	// Sanity: the env global applies when --no-config is absent.783	out, err := runSCCDir(t, dir, []string{SccConfigEnv + "=" + global})784	if err != nil {785		t.Fatal(err)786	}787	if !strings.Contains(out, "Language,Lines,Code") {788		t.Fatalf("env global should apply without --no-config, output:\n%s", out)789	}790791	// --no-config must disable the env global (back to tabular default).792	out, err = runSCCDir(t, dir, []string{SccConfigEnv + "=" + global}, "--no-config")793	if err != nil {794		t.Fatal(err)795	}796	if strings.Contains(out, "Language,Lines,Code") {797		t.Errorf("--no-config should disable the SCC_CONFIG_PATH global, output:\n%s", out)798	}799}800801// TestConfigPresentCLIFormatMultiWrites is the --format-multi counterpart of802// TestConfigPresentCLICanWrite: with config present (so the two-mode write split803// engages), a genuine-CLI --format-multi must still write its file. The §5804// capability model blocks config from writing, not the CLI; resolveWriteFlags805// must therefore source --format-multi from the genuine CLI too, not just -o.806func TestConfigPresentCLIFormatMultiWrites(t *testing.T) {807	dir := writeSccConfig(t, "--no-cocomo\n")808	target := filepath.Join(dir, "multi.csv")809	_, err := runSCCDir(t, dir, noGlobalConfig, "--format-multi", "csv:"+target)810	if err != nil {811		t.Fatal(err)812	}813	if info, statErr := os.Stat(target); statErr != nil || info.Size() == 0 {814		t.Errorf("genuine CLI --format-multi should write the file even with config present")815	}816}817818func TestNoConfigFastPathWritesFile(t *testing.T) {819	// No config present: -o on the CLI writes exactly as today (fast path).820	dir := t.TempDir()821	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {822		t.Fatal(err)823	}824	target := filepath.Join(dir, "out.csv")825	if _, err := runSCCDir(t, dir, noGlobalConfig, "-f", "csv", "-o", target); err != nil {826		t.Fatal(err)827	}828	if info, statErr := os.Stat(target); statErr != nil || info.Size() == 0 {829		t.Errorf("no-config fast path: CLI -o should write the file")830	}831}

Code quality findings 40

Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for name, want := range cases {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte(content), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nfunc main() {}\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
sub := filepath.Join(dir, "sub")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(sub, "x.go"), []byte("package x\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if _, statErr := os.Stat(filepath.Join(dir, "evil.csv")); statErr == nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
target := filepath.Join(dir, "good.csv")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
cfg := filepath.Join(dir, "team.sccconfig")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
target := filepath.Join(dir, "out.csv")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
global := filepath.Join(dir, "global.sccconfig")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
global := filepath.Join(dir, "iso.sccconfig")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
chained := filepath.Join(dir, "chained.sccconfig")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--config "+chained+"\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
out, err := runSCCDir(t, dir, noGlobalConfig, "--config", filepath.Join(dir, "does-not-exist.sccconfig"))
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
global := filepath.Join(dir, "from-atfile.sccconfig")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
atFile := filepath.Join(dir, "flags.txt")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.Mkdir(filepath.Join(dir, d), 0755); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, f), []byte("package x\n"), 0644); err != nil {
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
if err := os.WriteFile(filepath.Join(dir, f), []byte("package x\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--exclude-dir vendor\n"), 0644); err != nil {
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
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--exclude-dir vendor\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
atFile := filepath.Join(dir, "flags.txt")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "my file.go"), []byte("package x\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
atFile := filepath.Join(dir, "flags.txt")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.Mkdir(filepath.Join(root, ".git"), 0755); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(root, ".sccconfig"), []byte("--format csv\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
sub := filepath.Join(root, "sub")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(sub, "x.go"), []byte("package x\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
missing := filepath.Join(dir, "does-not-exist.sccconfig")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
global := filepath.Join(dir, "global.sccconfig")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
target := filepath.Join(dir, "multi.csv")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
target := filepath.Join(dir, "out.csv")

Get this view in your editor

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