diff --git a/Makefile b/Makefile index b31bb73d08..4844d7a3c1 100644 --- a/Makefile +++ b/Makefile @@ -65,9 +65,13 @@ wsfix: links: ./tools/update_github_links.py +.PHONY: deadcode +deadcode: + ./tools/check_deadcode.py + # Checks other than 'fmt' and 'lint'; these are fast, so can be run first .PHONY: checks -checks: tidy ws links +checks: tidy ws links deadcode .PHONY: install-pythons diff --git a/bundle/internal/schema/main_test.go b/bundle/internal/schema/main_test.go index 1b655d2e16..b41094ba4d 100644 --- a/bundle/internal/schema/main_test.go +++ b/bundle/internal/schema/main_test.go @@ -124,26 +124,3 @@ func getAnnotations(path string) (annotation.File, error) { err = yaml.Unmarshal(b, &data) return data, err } - -func DisabledTestNoDuplicatedAnnotations(t *testing.T) { - // Check for duplicated annotations in annotation files - files := []string{ - "annotations_openapi_overrides.yml", - "annotations.yml", - } - - annotations := map[string]string{} - for _, file := range files { - annotationsFile, err := getAnnotations(file) - assert.NoError(t, err) - for typ, props := range annotationsFile { - for prop := range props { - key := typ + "_" + prop - if prevFile, ok := annotations[key]; ok { - t.Errorf("Annotation `%s` is duplicated in %s and %s", key, prevFile, file) - } - annotations[key] = file - } - } - } -} diff --git a/bundle/libraries/upload.go b/bundle/libraries/upload.go index cb3ff2faf0..b292fe43b7 100644 --- a/bundle/libraries/upload.go +++ b/bundle/libraries/upload.go @@ -30,13 +30,6 @@ func Upload(libs map[string][]LocationToUpdate) bundle.Mutator { } } -func UploadWithClient(libs map[string][]LocationToUpdate, client filer.Filer) bundle.Mutator { - return &upload{ - libs: libs, - client: client, - } -} - type upload struct { client filer.Filer libs map[string][]LocationToUpdate diff --git a/cmd/bundle/utils/utils.go b/cmd/bundle/utils/utils.go index a7568b8f6e..3c4bd1a5b9 100644 --- a/cmd/bundle/utils/utils.go +++ b/cmd/bundle/utils/utils.go @@ -4,10 +4,6 @@ import ( "context" "github.com/databricks/cli/bundle" - bundleenv "github.com/databricks/cli/bundle/env" - "github.com/databricks/cli/bundle/phases" - "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/env" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" ) @@ -20,85 +16,3 @@ func configureVariables(cmd *cobra.Command, b *bundle.Bundle, variables []string } }) } - -// getTargetFromCmd returns the target name from command flags or environment. -func getTargetFromCmd(cmd *cobra.Command) string { - // Check command line flag first - if flag := cmd.Flag("target"); flag != nil { - if value := flag.Value.String(); value != "" { - return value - } - } - - // Check deprecated environment flag - if flag := cmd.Flag("environment"); flag != nil { - if value := flag.Value.String(); value != "" { - return value - } - } - - // Fall back to environment variable - target, _ := bundleenv.Target(cmd.Context()) - return target -} - -// ReloadBundle reloads the bundle configuration without modifying the command context. -// This is useful when you need to refresh the bundle configuration after changes -// without side effects like setting values on the context. -func ReloadBundle(cmd *cobra.Command) *bundle.Bundle { - ctx := cmd.Context() - - // Load the bundle configuration fresh from the filesystem - b := bundle.MustLoad(ctx) - if b == nil || logdiag.HasError(ctx) { - return b - } - - // Load the target configuration - if target := getTargetFromCmd(cmd); target == "" { - phases.LoadDefaultTarget(ctx, b) - } else { - phases.LoadNamedTarget(ctx, b, target) - } - - if logdiag.HasError(ctx) { - return b - } - - // Configure the workspace profile if provided - configureProfile(cmd, b) - - // Configure variables if provided - variables, err := cmd.Flags().GetStringSlice("var") - if err != nil { - logdiag.LogDiag(ctx, diag.FromErr(err)[0]) - return b - } - configureVariables(cmd, b, variables) - return b -} - -// configureProfile applies the profile flag to the bundle. -func configureProfile(cmd *cobra.Command, b *bundle.Bundle) { - profile := getProfileFromCmd(cmd) - if profile == "" { - return - } - - bundle.ApplyFuncContext(cmd.Context(), b, func(ctx context.Context, b *bundle.Bundle) { - b.Config.Workspace.Profile = profile - }) -} - -// getProfileFromCmd returns the profile from command flags or environment. -func getProfileFromCmd(cmd *cobra.Command) string { - // Check command line flag first - if flag := cmd.Flag("profile"); flag != nil { - if value := flag.Value.String(); value != "" { - return value - } - } - - // Fall back to environment variable - return env.Get(cmd.Context(), "DATABRICKS_CONFIG_PROFILE") -} diff --git a/experimental/aitools/lib/installer/installer.go b/experimental/aitools/lib/installer/installer.go index 828c458bd8..6ec9d467e3 100644 --- a/experimental/aitools/lib/installer/installer.go +++ b/experimental/aitools/lib/installer/installer.go @@ -67,14 +67,6 @@ type InstallOptions struct { Scope string // ScopeGlobal or ScopeProject (default: global) } -// FetchManifest fetches the skills manifest from the skills repo. -// This is a convenience wrapper that uses the default GitHubManifestSource. -func FetchManifest(ctx context.Context) (*Manifest, error) { - src := &GitHubManifestSource{} - ref := GetSkillsRef(ctx) - return src.FetchManifest(ctx, ref) -} - func fetchSkillFile(ctx context.Context, ref, skillName, filePath string) ([]byte, error) { url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s/%s/%s", skillsRepoOwner, skillsRepoName, ref, skillsRepoPath, skillName, filePath) @@ -303,19 +295,6 @@ func InstallAllSkills(ctx context.Context) error { return InstallSkillsForAgents(ctx, src, installed, InstallOptions{}) } -// InstallSkill installs a single skill by name for all detected agents. -func InstallSkill(ctx context.Context, skillName string) error { - installed := agents.DetectInstalled(ctx) - if len(installed) == 0 { - printNoAgentsDetected(ctx) - return nil - } - - PrintInstallingFor(ctx, installed) - src := &GitHubManifestSource{} - return InstallSkillsForAgents(ctx, src, installed, InstallOptions{SpecificSkills: []string{skillName}}) -} - // PrintInstallingFor prints the "Installing..." header with agent names. func PrintInstallingFor(ctx context.Context, targetAgents []*agents.Agent) { names := make([]string, len(targetAgents)) diff --git a/experimental/ssh/internal/keys/secrets.go b/experimental/ssh/internal/keys/secrets.go index d4e00d10ba..76d44da538 100644 --- a/experimental/ssh/internal/keys/secrets.go +++ b/experimental/ssh/internal/keys/secrets.go @@ -67,17 +67,3 @@ func putSecret(ctx context.Context, client *databricks.WorkspaceClient, scope, k } return nil } - -// PutSecretInScope creates the secret scope if needed and stores the secret. -// sessionID is the unique identifier for the session (cluster ID for dedicated clusters, connection name for serverless). -func PutSecretInScope(ctx context.Context, client *databricks.WorkspaceClient, sessionID, key, value string) (string, error) { - scopeName, err := CreateKeysSecretScope(ctx, client, sessionID) - if err != nil { - return "", err - } - err = putSecret(ctx, client, scopeName, key, value) - if err != nil { - return "", err - } - return scopeName, nil -} diff --git a/internal/testcli/golden.go b/internal/testcli/golden.go deleted file mode 100644 index eca4c1390b..0000000000 --- a/internal/testcli/golden.go +++ /dev/null @@ -1,24 +0,0 @@ -package testcli - -import ( - "context" - "fmt" - - "github.com/databricks/cli/internal/testutil" - "github.com/databricks/cli/libs/testdiff" - "github.com/stretchr/testify/assert" -) - -func captureOutput(t testutil.TestingT, ctx context.Context, args []string) string { - t.Helper() - r := NewRunner(t, ctx, args...) - stdout, stderr, err := r.Run() - assert.NoError(t, err) - return stderr.String() + stdout.String() -} - -func AssertOutput(t testutil.TestingT, ctx context.Context, args []string, expectedPath string) { - t.Helper() - out := captureOutput(t, ctx, args) - testdiff.AssertOutput(t, ctx, out, fmt.Sprintf("Output from %v", args), expectedPath) -} diff --git a/internal/testutil/helpers.go b/internal/testutil/helpers.go index f5afb51fd5..b5a257e2ff 100644 --- a/internal/testutil/helpers.go +++ b/internal/testutil/helpers.go @@ -25,7 +25,3 @@ func RandomName(prefix ...string) string { out += strings.ReplaceAll(uuid.New().String(), "-", "") return out } - -func ReplaceWindowsLineEndings(s string) string { - return strings.ReplaceAll(s, "\r\n", "\n") -} diff --git a/libs/apps/prompt/listers.go b/libs/apps/prompt/listers.go index 2757539479..5052744eb5 100644 --- a/libs/apps/prompt/listers.go +++ b/libs/apps/prompt/listers.go @@ -88,28 +88,6 @@ func ListSecretKeys(ctx context.Context, scope string) ([]ListItem, error) { return out, nil } -// ListSQLWarehousesItems returns SQL warehouses as ListItems (reuses same API as ListSQLWarehouses). -func ListSQLWarehousesItems(ctx context.Context) ([]ListItem, error) { - w, err := workspaceClient(ctx) - if err != nil { - return nil, err - } - iter := w.Warehouses.List(ctx, sql.ListWarehousesRequest{}) - whs, err := listing.ToSlice(ctx, iter) - if err != nil { - return nil, err - } - out := make([]ListItem, 0, min(len(whs), maxListResults)) - for _, wh := range whs { - label := wh.Name - if wh.State != "" { - label = fmt.Sprintf("%s (%s)", wh.Name, wh.State) - } - out = append(out, ListItem{ID: wh.Id, Label: label}) - } - return capResults(out), nil -} - // ListSchemas returns UC schemas within a catalog as selectable items. func ListSchemas(ctx context.Context, catalogName string) ([]ListItem, error) { w, err := workspaceClient(ctx) diff --git a/libs/apps/prompt/prompt.go b/libs/apps/prompt/prompt.go index 4a32c2e861..1b10f15024 100644 --- a/libs/apps/prompt/prompt.go +++ b/libs/apps/prompt/prompt.go @@ -467,20 +467,6 @@ func promptForPagedResource(ctx context.Context, r manifest.Resource, required b return singleValueResult(r, value), nil } -// PromptForWarehouse shows a picker to select a SQL warehouse. -func PromptForWarehouse(ctx context.Context) (string, error) { - var items []ListItem - err := RunWithSpinnerCtx(ctx, "Fetching SQL warehouses...", func() error { - var fetchErr error - items, fetchErr = ListSQLWarehousesItems(ctx) - return fetchErr - }) - if err != nil { - return "", fmt.Errorf("failed to fetch SQL warehouses: %w", err) - } - return PromptFromList(ctx, "Select SQL Warehouse", "no SQL warehouses found. Create one in your workspace first", items, true) -} - // resourceTitle returns a prompt title for a resource, including the plugin name // for context when available (e.g. "Select SQL Warehouse for Analytics"). func resourceTitle(fallback string, r manifest.Resource) string { diff --git a/libs/calladapt/validate_test.go b/libs/calladapt/validate_test.go index 41a4ce0e0b..c0a4482be2 100644 --- a/libs/calladapt/validate_test.go +++ b/libs/calladapt/validate_test.go @@ -17,13 +17,11 @@ type testIface interface { type partialType struct{} func (*partialType) Foo() {} -func (*partialType) baz() {} //nolint:unused type goodType struct{} func (*goodType) Foo() {} func (*goodType) Bar() {} -func (*goodType) baz() {} //nolint:unused type badType struct{} diff --git a/libs/cmdio/io.go b/libs/cmdio/io.go index d54840d0f0..5477cd3512 100644 --- a/libs/cmdio/io.go +++ b/libs/cmdio/io.go @@ -53,11 +53,6 @@ func NewIO(ctx context.Context, outputFormat flags.Output, in io.Reader, out, er } } -func IsInteractive(ctx context.Context) bool { - c := fromContext(ctx) - return c.capabilities.SupportsInteractive() -} - func IsPromptSupported(ctx context.Context) bool { c := fromContext(ctx) return c.capabilities.SupportsPrompt() diff --git a/libs/cmdio/render.go b/libs/cmdio/render.go index c344c3d028..d1c81eabed 100644 --- a/libs/cmdio/render.go +++ b/libs/cmdio/render.go @@ -271,21 +271,11 @@ func RenderIterator[T any](ctx context.Context, i listing.Iterator[T]) error { func RenderWithTemplate(ctx context.Context, v any, headerTemplate, template string) error { c := fromContext(ctx) if _, ok := v.(listingInterface); ok { - panic("use RenderIteratorWithTemplate instead") + panic("listings must use RenderIterator, not RenderWithTemplate") } return renderWithTemplate(ctx, newRenderer(v), c.outputFormat, c.out, headerTemplate, template) } -func RenderIteratorWithTemplate[T any](ctx context.Context, i listing.Iterator[T], headerTemplate, template string) error { - c := fromContext(ctx) - return renderWithTemplate(ctx, newIteratorRenderer(i), c.outputFormat, c.out, headerTemplate, template) -} - -func RenderIteratorJson[T any](ctx context.Context, i listing.Iterator[T]) error { - c := fromContext(ctx) - return renderWithTemplate(ctx, newIteratorRenderer(i), c.outputFormat, c.out, c.headerTemplate, c.template) -} - var renderFuncMap = template.FuncMap{ // we render colored output if stdout is TTY, otherwise we render text. // in the future we'll check if we can explicitly check for stderr being diff --git a/libs/dagrun/dagrun.go b/libs/dagrun/dagrun.go index 0ccefaa289..de2fa44b8e 100644 --- a/libs/dagrun/dagrun.go +++ b/libs/dagrun/dagrun.go @@ -23,8 +23,6 @@ func NewGraph() *Graph { } } -func (g *Graph) Size() int { return len(g.Nodes) } - func (g *Graph) AddNode(n string) { if _, ok := g.Adj[n]; !ok { g.Adj[n] = nil @@ -32,8 +30,6 @@ func (g *Graph) AddNode(n string) { } } -func (g *Graph) HasNode(n string) bool { _, ok := g.Adj[n]; return ok } - func (g *Graph) AddDirectedEdge(from, to, label string) { g.AddNode(from) g.AddNode(to) diff --git a/libs/databrickscfg/profile/context.go b/libs/databrickscfg/profile/context.go index fa4d2ad8ac..910e787669 100644 --- a/libs/databrickscfg/profile/context.go +++ b/libs/databrickscfg/profile/context.go @@ -4,10 +4,6 @@ import "context" var profiler int -func WithProfiler(ctx context.Context, p Profiler) context.Context { - return context.WithValue(ctx, &profiler, p) -} - func GetProfiler(ctx context.Context) Profiler { p, ok := ctx.Value(&profiler).(Profiler) if !ok { diff --git a/libs/dyn/dynassert/dump.go b/libs/dyn/dynassert/dump.go deleted file mode 100644 index 82b2c2b970..0000000000 --- a/libs/dyn/dynassert/dump.go +++ /dev/null @@ -1,60 +0,0 @@ -package dynassert - -import ( - "fmt" - "strings" - - "github.com/databricks/cli/libs/dyn" -) - -// Dump returns the Go code to recreate the given value. -func Dump(v dyn.Value) string { - var sb strings.Builder - dump(v, &sb) - return sb.String() -} - -func dump(v dyn.Value, sb *strings.Builder) { - sb.WriteString("dyn.NewValue(\n") - - switch v.Kind() { - case dyn.KindMap: - sb.WriteString("map[string]dyn.Value{") - m := v.MustMap() - for _, p := range m.Pairs() { - fmt.Fprintf(sb, "\n%q: ", p.Key.MustString()) - dump(p.Value, sb) - sb.WriteByte(',') - } - sb.WriteString("\n},\n") - case dyn.KindSequence: - sb.WriteString("[]dyn.Value{\n") - for _, e := range v.MustSequence() { - dump(e, sb) - sb.WriteByte(',') - } - sb.WriteString("},\n") - case dyn.KindString: - fmt.Fprintf(sb, "%q,\n", v.MustString()) - case dyn.KindBool: - fmt.Fprintf(sb, "%t,\n", v.MustBool()) - case dyn.KindInt: - fmt.Fprintf(sb, "%d,\n", v.MustInt()) - case dyn.KindFloat: - fmt.Fprintf(sb, "%f,\n", v.MustFloat()) - case dyn.KindTime: - fmt.Fprintf(sb, "dyn.NewTime(%q),\n", v.MustTime().String()) - case dyn.KindNil: - sb.WriteString("nil,\n") - default: - panic(fmt.Sprintf("unhandled kind: %v", v.Kind())) - } - - // Add location - sb.WriteString("[]dyn.Location{") - for _, l := range v.Locations() { - fmt.Fprintf(sb, "{File: %q, Line: %d, Column: %d},", l.File, l.Line, l.Column) - } - sb.WriteString("},\n") - sb.WriteString(")") -} diff --git a/libs/dyn/pattern.go b/libs/dyn/pattern.go index 2a15d12cc3..4aefb01542 100644 --- a/libs/dyn/pattern.go +++ b/libs/dyn/pattern.go @@ -1,7 +1,6 @@ package dyn import ( - "errors" "fmt" "slices" "strings" @@ -108,11 +107,6 @@ func (e expectedMapError) Error() string { return fmt.Sprintf("expected a map at %q, found %s", e.p, e.v.Kind()) } -func IsExpectedMapError(err error) bool { - var target expectedMapError - return errors.As(err, &target) -} - type expectedSequenceError struct { p Path v Value @@ -122,11 +116,6 @@ func (e expectedSequenceError) Error() string { return fmt.Sprintf("expected a sequence at %q, found %s", e.p, e.v.Kind()) } -func IsExpectedSequenceError(err error) bool { - var target expectedSequenceError - return errors.As(err, &target) -} - // This function implements the patternComponent interface. func (c anyKeyComponent) visit(v Value, prefix Path, suffix Pattern, opts visitOptions) (Value, error) { m, ok := v.AsMap() diff --git a/libs/dyn/visit.go b/libs/dyn/visit.go index 15ea0af5c7..1822c7db65 100644 --- a/libs/dyn/visit.go +++ b/libs/dyn/visit.go @@ -63,11 +63,6 @@ func (e expectedMapToIndexError) Error() string { return fmt.Sprintf("expected a map to index %q, found %s", e.p, e.v.Kind()) } -func IsExpectedMapToIndexError(err error) bool { - var target expectedMapToIndexError - return errors.As(err, &target) -} - type expectedSequenceToIndexError struct { p Path v Value @@ -77,11 +72,6 @@ func (e expectedSequenceToIndexError) Error() string { return fmt.Sprintf("expected a sequence to index %q, found %s", e.p, e.v.Kind()) } -func IsExpectedSequenceToIndexError(err error) bool { - var target expectedSequenceToIndexError - return errors.As(err, &target) -} - type visitOptions struct { // The function to apply to the value once found. // diff --git a/libs/fileset/fileset.go b/libs/fileset/fileset.go index 26ae2e8600..ec9dcdeb79 100644 --- a/libs/fileset/fileset.go +++ b/libs/fileset/fileset.go @@ -61,11 +61,6 @@ func Empty() *FileSet { return &FileSet{} } -// Ignorer returns the [FileSet]'s current ignorer. -func (w *FileSet) Ignorer() Ignorer { - return w.ignore -} - // SetIgnorer sets the [Ignorer] interface for this [FileSet]. func (w *FileSet) SetIgnorer(ignore Ignorer) { w.ignore = ignore diff --git a/libs/git/fileset.go b/libs/git/fileset.go index e6c6518931..da2a6cec78 100644 --- a/libs/git/fileset.go +++ b/libs/git/fileset.go @@ -33,14 +33,6 @@ func NewFileSetAtRoot(ctx context.Context, root vfs.Path, paths ...[]string) (*F return NewFileSet(ctx, root, root, paths...) } -func (f *FileSet) IgnoreFile(file string) (bool, error) { - return f.view.IgnoreFile(file) -} - -func (f *FileSet) IgnoreDirectory(dir string) (bool, error) { - return f.view.IgnoreDirectory(dir) -} - func (f *FileSet) Files() ([]fileset.File, error) { f.view.repo.taintIgnoreRules() return f.fileset.Files() diff --git a/libs/log/logger.go b/libs/log/logger.go index d77232f6c5..74347b1ab1 100644 --- a/libs/log/logger.go +++ b/libs/log/logger.go @@ -27,15 +27,6 @@ func log(ctx context.Context, logger *slog.Logger, level slog.Level, msg string) _ = logger.Handler().Handle(ctx, r) } -// Trace logs a string using the context-local or global logger. -func Trace(ctx context.Context, msg string) { - logger := GetLogger(ctx) - if !logger.Enabled(ctx, LevelTrace) { - return - } - log(ctx, logger, LevelTrace, msg) -} - // Debug logs a string using the context-local or global logger. func Debug(ctx context.Context, msg string) { logger := GetLogger(ctx) @@ -63,15 +54,6 @@ func Warn(ctx context.Context, msg string) { log(ctx, logger, LevelWarn, msg) } -// Error logs a string using the context-local or global logger. -func Error(ctx context.Context, msg string) { - logger := GetLogger(ctx) - if !logger.Enabled(ctx, LevelError) { - return - } - log(ctx, logger, LevelError, msg) -} - // Tracef logs a formatted string using the context-local or global logger. func Tracef(ctx context.Context, format string, v ...any) { logger := GetLogger(ctx) diff --git a/libs/process/opts.go b/libs/process/opts.go index dd06675168..a9848cebde 100644 --- a/libs/process/opts.go +++ b/libs/process/opts.go @@ -38,24 +38,6 @@ func WithDir(dir string) execOption { } } -func WithStdoutPipe(dst *io.ReadCloser) execOption { - return func(_ context.Context, c *exec.Cmd) error { - outPipe, err := c.StdoutPipe() - if err != nil { - return err - } - *dst = outPipe - return nil - } -} - -func WithStdinReader(src io.Reader) execOption { - return func(_ context.Context, c *exec.Cmd) error { - c.Stdin = src - return nil - } -} - func WithStderrWriter(dst io.Writer) execOption { return func(_ context.Context, c *exec.Cmd) error { c.Stderr = dst diff --git a/libs/structs/structpath/path.go b/libs/structs/structpath/path.go index 90d1fdb6c6..5ae81019ee 100644 --- a/libs/structs/structpath/path.go +++ b/libs/structs/structpath/path.go @@ -637,15 +637,6 @@ func MustParsePath(s string) *PathNode { return path } -// MustParsePattern parses a pattern string and panics on error. Wildcards are allowed. -func MustParsePattern(s string) *PatternNode { - pattern, err := ParsePattern(s) - if err != nil { - panic(err) - } - return pattern -} - // isReservedFieldChar checks if character is reserved and cannot be used in field names func isReservedFieldChar(ch byte) bool { switch ch { diff --git a/libs/testdiff/golden.go b/libs/testdiff/golden.go index f49a5a24be..1e7fe99f0f 100644 --- a/libs/testdiff/golden.go +++ b/libs/testdiff/golden.go @@ -1,15 +1,8 @@ package testdiff import ( - "context" - "errors" "flag" - "io/fs" - "os" "strings" - - "github.com/databricks/cli/internal/testutil" - "github.com/stretchr/testify/assert" ) var OverwriteMode = false @@ -18,49 +11,6 @@ func init() { flag.BoolVar(&OverwriteMode, "update", false, "Overwrite golden files") } -func ReadFile(t testutil.TestingT, ctx context.Context, filename string) string { - t.Helper() - data, err := os.ReadFile(filename) - if errors.Is(err, fs.ErrNotExist) { - return "" - } - assert.NoError(t, err, "Failed to read %s", filename) - // On CI, on Windows \n in the file somehow end up as \r\n - return NormalizeNewlines(string(data)) -} - -func WriteFile(t testutil.TestingT, filename, data string) { - t.Helper() - t.Logf("Overwriting %s", filename) - err := os.WriteFile(filename, []byte(data), 0o644) - assert.NoError(t, err, "Failed to write %s", filename) -} - -func AssertOutput(t testutil.TestingT, ctx context.Context, out, outTitle, expectedPath string) { - t.Helper() - expected := ReadFile(t, ctx, expectedPath) - - out = ReplaceOutput(t, ctx, out) - - if out != expected { - AssertEqualTexts(t, expectedPath, outTitle, expected, out) - - if OverwriteMode { - WriteFile(t, expectedPath, out) - } - } -} - -func ReplaceOutput(t testutil.TestingT, ctx context.Context, out string) string { - t.Helper() - out = NormalizeNewlines(out) - replacements := GetReplacementsMap(ctx) - if replacements == nil { - t.Fatal("WithReplacementsMap was not called") - } - return replacements.Replace(out) -} - func NormalizeNewlines(input string) string { output := strings.ReplaceAll(input, "\r\n", "\n") return strings.ReplaceAll(output, "\r", "\n") diff --git a/tools/check_deadcode.py b/tools/check_deadcode.py new file mode 100755 index 0000000000..0cc97c8599 --- /dev/null +++ b/tools/check_deadcode.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.12" +# /// +""" +Deadcode checker for the Databricks CLI. + +Runs the 'deadcode' tool (golang.org/x/tools/cmd/deadcode) to find functions +that are unreachable from main() or test entry points. Since the CLI is a +product (not a library), any unreachable function is dead code. + +Suppression mechanisms +====================== + +1. Directory exclusions (EXCLUDED_DIRS below): + Entire directories can be excluded. Use this for directories where + everything is a false positive. Example: libs/gorules/ contains lint + rule definitions loaded by golangci-lint's ruleguard engine, not + through Go's call graph. + +2. Inline comments: + Add "//deadcode:allow " above a function to suppress a + specific finding. The comment can appear on the line directly above + the func keyword, or above a doc comment block. The script scans + up to 5 lines above the reported line, stopping at a blank line. + + Example: + + //deadcode:allow loaded by golangci-lint ruleguard, not via Go imports + // ProcessRule applies a lint rule. + func MyLintRule(m dsl.Matcher) { + + This matches the //nolint: pattern Go developers already know. +""" + +import re +import subprocess +import sys + +# Directories to exclude entirely. Each entry is matched as a substring +# of the file path in deadcode output. +EXCLUDED_DIRS = [ + "libs/gorules/", # Lint rule definitions loaded by golangci-lint's ruleguard + "bundle/internal/tf/schema/", # Generated from Terraform provider schema +] + +ALLOW_COMMENT = "//deadcode:allow" + + +def main(): + result = subprocess.run( + ["go", "tool", "-modfile=tools/go.mod", "deadcode", "-test", "./..."], + capture_output=True, + text=True, + ) + if result.returncode != 0 and not result.stdout.strip(): + print("deadcode failed:\n", file=sys.stderr) + print(result.stderr, file=sys.stderr) + sys.exit(1) + + output = result.stdout.strip() + if not output: + print("No dead code found.") + return + + lines = output.split("\n") + violations = [] + + for line in lines: + if any(line.startswith(d) or ("/" + d) in line for d in EXCLUDED_DIRS): + continue + + match = re.match(r"(.+?):(\d+):\d+:", line) + if not match: + violations.append(line) + continue + + filepath = match.group(1) + lineno = int(match.group(2)) + + try: + with open(filepath) as f: + file_lines = f.readlines() + # Scan up to 5 lines above the reported line, stopping at a + # blank line. This handles doc comments between the allow + # comment and the func keyword. + suppressed = False + start = max(0, lineno - 6) + for check_line in file_lines[start : lineno - 1]: + stripped = check_line.strip() + if not stripped: + break + if ALLOW_COMMENT in stripped: + suppressed = True + break + if suppressed: + continue + except (OSError, IndexError): + pass + + violations.append(line) + + if not violations: + print("No dead code found.") + return + + print("Dead code found:\n") + for v in violations: + print(f" {v}") + print(f"\n{len(violations)} unreachable function(s) found.") + print("\nTo suppress, add a comment on the line above the function:") + print(" //deadcode:allow ") + print("\nOr add a directory exclusion in tools/check_deadcode.py.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/go.mod b/tools/go.mod index a961b14996..3755d674db 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -208,6 +208,7 @@ require ( golang.org/x/mod v0.28.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect + golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/tools v0.37.0 // indirect @@ -224,5 +225,6 @@ require ( tool ( github.com/golangci/golangci-lint/v2/cmd/golangci-lint github.com/google/yamlfmt/cmd/yamlfmt + golang.org/x/tools/cmd/deadcode gotest.tools/gotestsum ) diff --git a/tools/go.sum b/tools/go.sum index 6c6b89daf9..43a76d00c4 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -814,6 +814,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=