Tenet’s are 10x shorter and faster to write than Linters

Comparison of size in code

Lines (CL) Lines (L) Words (CL) Words (L) Bytes (CL) Bytes (L) Byte % (CL/L) Tenet name (CL) linter name (L)
18 681 49 2084 524 15616 3.36% unconvert unconvert
19 110 64 275 580 2198 26.39% init gochecknoinits
18 136 67 353 623 2307 27.00% bool-param nofuncflags
18 137 63 310 512 2168 23.62% todo godox
42 753 115 2387 1154 20307 5.68% tested blanket
CL
CodeLingo
L
Linter

Making tenets with CLQL that do the job of existing linters

I started learning Go and CodeLingo / CLQL at the same time, so while I have found it generally easy and straightforward to create these tenets, I feel like I could’ve knocked them out even faster if I didn’t need to look up answers to questions such as ‘what is an interface in golang?’, for example. The process has been intuitive; I think in part to having a good naming convention. I’ve had to learn what CodeLingo is, what a ‘tenet’ and a ‘flow’ is. Long story short, they are just as what they sound like they are.

Defintions taken from the project docs

tenet
A Tenet is an encoded project-specific best practice used to guide development.

(an encoded best practice)

flow
A Flow is an automated development workflow that leverages Tenets to do some task, for example automating code reviews.

(an automated workflow)

The general process of writing one tenet

Writing the CLQL

What is simple to implement in CodeLingo may be difficult for 3rd party linters, however, as this write-up should demonstrate.
Writing a tenet to describe an antipattern might be trivial in CLQL, but the corresponding linter may be orders of magnitude more complex.

The first step is usually to generate a verbose CLQL query from the CodeLingo playground.
To do this you select the part of the code you would like to match and then click the generate1 button.
The next step is to refine that query into something that captures the logic of what you want a tenet to match.
Typically, the generated query comprises the bulk of the code you end up with. You can therefore choose what to leave in, or add new logic, depending on the needs of the tenet.


Before: Generated CLQL

This generated query contains a greater number of facts than we really need, so we can remove some of them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import codelingo/ast/go

go.file(depth = any):
  go.decls:
    @playground.highlight
    go.func_decl:
      go.ident:
        child_count == 0
        name == "init"
        private == "true"
        public == "false"
      go.func_type:
        go.field_list:
          child_count == 0

Refining the CLQL

The process of crafting CLQL is mainly subtractive, especially for simple tenets.

@@ -2,13 +2,10 @@ import codelingo/ast/go
 
 go.file(depth = any):
   go.decls:
-    @playground.highlight
     go.func_decl:
+      @review comment
       go.ident:
-        child_count == 0
         name == "init"
-        private == "true"
-        public == "false"
       go.func_type:
         go.field_list:
           child_count == 0

After: Refined CLQL

Short and sweet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import codelingo/ast/go

go.file(depth = any):
  go.decls:
    go.func_decl:
      @review comment
      go.ident:
        name == "init"
      go.func_type:
        go.field_list:
          child_count == 0

See the full article here for further explanation.

Results

Comparison of size Unit tests

Lines (CL) Lines (L) Words (CL) Words (L) Bytes (CL) Bytes (L) Byte % (CL/L) Tenet name (CL) linter name (L)
13 N/A2 25 N/A2 246 N/A2 N/A2 unconvert unconvert
32 201 64 407 333 3167 10.51% init gochecknoinits
16 24 27 36 166 261 63.60% bool-param nofuncflags
29 130 80 407 440 3000 14.67% todo godox
14 124 25 185 156 1229 12.69% tested blanket
N/A
The original linter did not contain unit tests.
tenet name linter name tenet code forge linter code description
init gochecknoinits codelingo.yaml GitHub leighmcculloch/gochecknoinits Check that no init functions are present in Go code.
unconvert unconvert codelingo.yaml GitHub mdempsky/unconvert Remove unnecessary type conversions from Go source
bool-param nofuncflags codelingo.yaml GitHub fsamin/nofuncflags because flag arguments are ugly
todo godox codelingo.yaml GitHub 766b/godox extract speficic comments from Go code based on keywords
tested3 blanket3 codelingo.yaml GitLab verygoodsoftwarenotvirus/blanket a coverage helper tool

The work it took to write these tenets

tenet name time to write min clicks actual clicks4 reason for generate query1 click/s reason for time spent greater or less than 10 mins
init 10 mins 1 1 to find an initial fact for a top-level init function
unconvert 20 mins 1 2 to generalise unit test to any type conversion to create string variable to match function name with ident type
bool-param 5 mins 1 1 to generate initial query the generated query was ~= the finished tenet
todo 10 mins 1 1 to find the CLQL fact for comment
tested3 20 mins 1 2 to find the initial query for a filename with identifier learning to use CLQL functions
min clicks
The number of times I needed to press generate query1 to discover the CLQL syntax I needed.
actual clicks
The approximate number of times I ended up to pressing generate query, for exploratory purposes.

The work it took to write their unit tests

tenet name time to write4 generate1 clicks4 additional time reason for additional time test runs5 reason for additional testing of unit tests
init 10 mins 1 2
unconvert 10 mins 2 10 mins to find example code for unit test 2
bool-param 5 mins 1 2
todo 10 mins 1 2
tested3 20 mins 1 4 ensure multi-file unit tests are working as expected

A nice surprise

  • Blanket (the original linter) has a limitation. It can handle single-level selector expressions with great ease, but it doesn’t recursively dive into those selectors for a number of reasons.

    The ‘tested’ tenet, however, can find if said method was called with arbitrary nesting.

    It doesn’t actually look at the function call to make sure it is the same object that is calling. It only checks that the method being called has the same name and that it is being called within a test function and file of appropriate name.

    So it doesn’t have the same recursive limitation as blanket, but may give rise to false positives in some circumstances.

Read the full article here.


  1. This is what the button looks like. ↩︎

  2. The original linter did not contain unit tests. ↩︎

  3. See caveats. ↩︎

  4. These are approximations. ↩︎

  5. The number of times I had to test the tenet. ↩︎