decouple

package module
v0.4.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 19, 2024 License: MIT Imports: 10 Imported by: 0

README

Decouple - find overspecified function parameters in Go code

Go Reference Go Report Card Tests Coverage Status Mentioned in Awesome Go

This is decouple, a Go package and command that analyzes your Go code to find “overspecified” function parameters.

A parameter is overspecified, and eligible for “decoupling,” if it has a more-specific type than it actually needs.

For example, if your function takes a *os.File parameter, but it’s only ever used for its Read method, it could be specified as an abstract io.Reader instead.

Why decouple?

When you decouple a function parameter from its too-specific type, you broaden the set of values on which it can operate.

You also make it easier to test. For a simple example, suppose you’re testing this function:

func CountLines(f *os.File) (int, error) {
  var result int
  sc := bufio.NewScanner(f)
  for sc.Scan() {
    result++
  }
  return result, sc.Err()
}

Your unit test will need to open a testdata file and pass it to this function to get a result. But as decouple can tell you, f is only ever used as an io.Reader (the type of the argument to bufio.NewScanner).

If you were testing func CountLines(r io.Reader) (int, error) instead, the unit test can simply pass it something like strings.NewReader("a\nb\nc").

Installation

go install github.com/bobg/decouple/cmd/decouple@latest

Usage

decouple [-v] [-json] [DIR]

This produces a report about the Go packages rooted at DIR (the current directory by default). With -v, very verbose debugging output is printed along the way. With -json, the output is in JSON format.

The report will be empty if decouple has no findings. Otherwise, it will look something like this (without -json):

$ decouple
/home/bobg/kodigcs/handle.go:105:18: handleDir
    req: [Context]
    w: io.Writer
/home/bobg/kodigcs/handle.go:167:18: handleNFO
    req: [Context]
    w: [Header Write]
/home/bobg/kodigcs/handle.go:428:6: isStale
    t: [Before]
/home/bobg/kodigcs/imdb.go:59:6: parseIMDbPage
    cl: [Do]

This is the output when running decouple on the current commit of kodigcs. It’s saying that:

  • In the function handleDir, the req parameter is being used only for its Context method and so could be declared as interface{ Context() context.Context }, allowing objects other than *http.Request values to be passed in here (or, better still, the function could be rewritten to take a context.Context parameter instead);
  • Also in handleDir, w could be an io.Writer, allowing more types to be used than just http.ResponseWriter;
  • Similarly in handleNFO, req is used only for its Context method, and w for its Write and Header methods (more than io.Writer, but less than http.ResponseWriter);
  • Anything with a Before(time.Time) bool method could be used in isStale, it does not need to be limited to time.Time;
  • The *http.Client argument of parseIMDbPage is being used only for its Do method.

Note that, in the report, the presence of square brackets means “this is a set of methods,” while the absence of them means “this is an existing type that already has the right method set” (as in the io.Writer line in the example above). Decouple can’t always find a suitable existing type even when one exists, and if two or more types match, it doesn’t always choose the best one.

The same report with -json specified looks like this:

{
  "PackageName": "main",
  "FileName": "/home/bobg/kodigcs/handle.go",
  "Line": 105,
  "Column": 18,
  "FuncName": "handleDir",
  "Params": [
    {
      "Name": "req",
      "Methods": [
        "Context"
      ]
    },
    {
      "Name": "w",
      "Methods": [
        "Write"
      ],
      "InterfaceName": "io.Writer"
    }
  ]
}
{
  "PackageName": "main",
  "FileName": "/home/bobg/kodigcs/handle.go",
  "Line": 167,
  "Column": 18,
  "FuncName": "handleNFO",
  "Params": [
    {
      "Name": "req",
      "Methods": [
        "Context"
      ]
    },
    {
      "Name": "w",
      "Methods": [
        "Header",
        "Write"
      ]
    }
  ]
}
{
  "PackageName": "main",
  "FileName": "/home/bobg/kodigcs/handle.go",
  "Line": 428,
  "Column": 6,
  "FuncName": "isStale",
  "Params": [
    {
      "Name": "t",
      "Methods": [
        "Before"
      ]
    }
  ]
}
{
  "PackageName": "main",
  "FileName": "/home/bobg/kodigcs/imdb.go",
  "Line": 59,
  "Column": 6,
  "FuncName": "parseIMDbPage",
  "Params": [
    {
      "Name": "cl",
      "Methods": [
        "Do"
      ]
    }
  ]
}

Performance note

Replacing overspecified function parameters with more-abstract ones, which this tool helps you to do, is often but not always the right thing, and it should not be done blindly.

Using Go interfaces can impose an abstraction penalty compared to using concrete types. Function arguments that could have been on the stack may end up in the heap, and method calls may involve a virtual-dispatch step.

In many cases this penalty is small and can be ignored, especially since the Go compiler may optimize some or all of it away. But in tight inner loops and other performance-critical code it is often preferable to operate only on concrete types when possible.

That said, avoid the fallacy of premature optimization. Write your code for clarity and utility first. Then sacrifice those for the sake of performance not in the places where you think they’ll make a difference, but in the places where you’ve measured that they’re needed.

Documentation

Overview

Decouple analyzes Go packages to find overspecified function parameters. If your function takes a *os.File for example, but only ever calls Read on it, the function can be rewritten to take an io.Reader. This generalizes the function, making it easier to test and decoupling it from whatever the source of the *os.File is.

Index

Constants

PkgMode is the minimal set of bit flags needed for the Config.Mode field of golang.org/x/go/packages for the result to be usable by a Checker.

Variables

This section is empty.

Functions

This section is empty.

Types

type Checker

type Checker struct {
	Verbose bool
	// contains filtered or unexported fields
}

Checker is the object that can analyze a directory tree of Go code, or a set of packages loaded with "golang.org/x/go/packages".Load, or a single such package, or a function or function parameter in one.

Set Verbose to true to get (very) verbose debugging output.

func NewCheckerFromDir

func NewCheckerFromDir(dir string) (Checker, error)

NewCheckerFromDir creates a new Checker containing packages loaded (using "golang.org/x/go/packages".Load) from the given directory tree.

func NewCheckerFromPackages

func NewCheckerFromPackages(pkgs []*packages.Package) Checker

NewCheckerFromPackages creates a new Checker containing the given packages, which should be the result of calling "golang.org/x/go/packages".Load with at least the bits in PkgMode set in the Config.Mode field.

func (Checker) Check

func (ch Checker) Check() ([]Tuple, error)

Check checks all the packages in the Checker. It analyzes the functions in them, looking for parameters with concrete types that could be interfaces instead. The result is a list of Tuples, one for each function checked that has parameters eligible for decoupling.

func (Checker) CheckFunc

func (ch Checker) CheckFunc(pkg *packages.Package, fndecl *ast.FuncDecl) (map[string]MethodMap, error)

CheckFunc checks a single function declaration, which should appear in the given package, which should be one of the packages contained in the Checker. The result is a map from parameter names eligible for decoupling to MethodMaps.

func (Checker) CheckPackage

func (ch Checker) CheckPackage(pkg *packages.Package) ([]Tuple, error)

CheckPackage checks a single package. It should be one of the packages contained in the Checker. The result is a list of Tuples, one for each function checked that has parameters eligible for decoupling.

func (Checker) CheckParam

func (ch Checker) CheckParam(pkg *packages.Package, fndecl *ast.FuncDecl, name *ast.Ident) (_ MethodMap, err error)

CheckParam checks a single named parameter in a given function declaration, which must apepar in the given package, which should be one of the packages in the Checker. The result is a MethodMap for the parameter, and may be nil if the parameter is not eligible for decoupling.

func (Checker) NameForMethods

func (ch Checker) NameForMethods(inp MethodMap) string

NameForMethods takes a MethodMap and returns the name of an interface defining exactly the methods in it, if it can find one among the packages in the Checker. If there are multiple such interfaces, one is chosen arbitrarily.

type MethodMap

type MethodMap = map[string]*types.Signature

MethodMap maps a set of method names to their calling signatures.

type Tuple

type Tuple struct {
	// F is the function declaration that this result is about.
	F *ast.FuncDecl

	// P is the package in which the function declaration appears.
	P *packages.Package

	// M is a map from the names of function parameters eligible for decoupling
	// to MethodMaps for each such parameter.
	M map[string]MethodMap
}

Tuple is the type of a result from Checker.Check and Checker.CheckPackage.

func (Tuple) Pos

func (t Tuple) Pos() token.Position

Pos computes the filename and offset of the function name of the Tuple.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL