A CLI Tool for CI/CD pipelines to prevent Golang spaghetti code


A Golang-based command line tool for CI/CD pipelines (and dev machines) that helps to prevent Go spaghetti code (a.k.a. big ball of mud).

Thankfully, in the Go world, the compiler already prevents circular dependencies between packages. So this tool has to care only about additional undesired dependencies.

I gave a talk that included the motivation for this tool and some (old) usage examples: 

Additionally, this tool documents the structure of a project in its configuration.

Usage of the Golang CLI Tool

You can simply call it with go run github.com/flowdev/spaghetti-cutter from anywhere inside your project.

The possible command line options are:

Usage of spaghetti-cutter:
  -e    don't report errors and don't exit with an error (shorthand)
  -noerror
        don't report errors and don't exit with an error
  -r string
        root directory of the project (shorthand) (default ".")
  -root string
        root directory of the project (default ".")

If no --root option is given the root directory is found by crawling up the directory tree starting at the current working directory. The first directory that contains the configuration file .spaghetti-cutter.hjson will be taken as project root.

The output looks like:

2020/09/10 09:37:08 INFO - configuration 'allowOnlyIn': `github.com/hjson/**`: `x/config` ; `golang.org/x/tools**`: `parse*`, `x/pkgs*`
2020/09/10 09:37:08 INFO - configuration 'allowAdditionally': `*_test`: `parse`
2020/09/10 09:37:08 INFO - configuration 'god': `main`
2020/09/10 09:37:08 INFO - configuration 'tool': `x/*`
2020/09/10 09:37:08 INFO - configuration 'db': ...
2020/09/10 09:37:08 INFO - configuration 'size': 1024
2020/09/10 09:37:08 INFO - configuration 'noGod': false
2020/09/10 09:37:08 INFO - root package: github.com/flowdev/spaghetti-cutter
2020/09/10 09:37:08 INFO - Size of package 'x/config': 699
2020/09/10 09:37:08 INFO - Size of package 'x/pkgs': 134
2020/09/10 09:37:08 INFO - Size of package 'deps': 401
2020/09/10 09:37:08 INFO - Size of package 'parse': 109
2020/09/10 09:37:08 INFO - Size of package 'size': 838
2020/09/10 09:37:08 INFO - Size of package 'x/dirs': 86
2020/09/10 09:37:08 INFO - Size of package '/': 202
2020/09/10 09:37:08 INFO - No errors found.

First, the configuration values and the root package are reported. So you can easily ensure that the correct configuration file is taken.

All package sizes are reported and last but not least, any violations are found. Since no error was found, the return code is 0.

A typical error message would be:

2020/09/10 10:31:14 ERROR - domain package 'pkg/shopping' isn't allowed to import package 'pkg/cart'

The return code is first From the output, you can see that

  • the package pkg/shopping is recognized as a standard domain package,
  • it imports the pkg/cart package and
  • there is no allowAdditionally configuration to allow this.

You can fix that by adding a bit of configuration.

Other non-zero return codes are possible for technical problems (unparsable code: 6, …). If appropriately used in the build pipeline, a non-zero return code will stop the build, and the problem has to be fixed first. So undesired imports (spaghetti relationships) are prevented.

Standard Use Case: Golang Web API

According to my own unscientific research, this tool was created with Web APIs in mind as that is what about 95% of all Gophers do.

So it offers special handling for the following cases:

  • Tools: Tool packages can be used everywhere except in other tool packages. But they aren’t allowed to import any other internal packages.
  • Database: DB packages can be used in other and standard (business) packages. Of course they can use tool packages but nothing else. Domain data structures can be either DB or tool packages.
  • Database sub-packages: Sub-packages of DB packages are allowed to only import tool packages like DB packages. Additionally, they aren’t allowed to be used anywhere else in the project. So you should use explicit configuration with explanations as comments (what the sub-packages contain and why they exist at all).
  • Tool sub-packages: Sub-packages of tool packages aren’t allowed to import any other internal package like tool packages. Additionally, they aren’t allowed to be used anywhere else in the project. So you should use explicit configuration with explanations as comments (what the sub-packages contain and why they exist).
  • God: A god package can see and use everything. You should use this with great care. main is the only default god package used if no explicit configuration is given. You should only rarely add more. You can switch main to a standard package with the noGod configuration key. This makes sense if you have got multiple main packages with different dependencies.

These cases needn’t be used and can be overridden with explicit configuration.

Configuration

It is mandatory to use a HJSON configuration file .spaghetti-cutter.hjson in the root directory of your project. This serves multiple purposes:

  • It helps the spaghetti-cutter to find the root directory of your project.
  • It saves you from retyping command line options again and again.
  • It is valuable documentation, especially for developers new to the project.

The configuration can have the following elements:

  • tooldb and god for tool, database and god packages as discussed above.
  • allowOnlyIn: for restricting a package to be used only in some packages (allow “key” package only in “value” packages).
  • allowAdditionally: for allowing additional dependencies (for “key” package allow additional “value” packages).
  • size: the maximum allowed size/complexity of a package. Default is 2048.
  • noGod: main It won’t be god package.

The size configuration key prevents a clever developer from just throwing all of the spaghetti code into a single package. With the spaghetti-cutter such things will become obvious and you can put them as technical dept into your back log.