Skip to content

Compare two golang interface definitions, e.g. check version in README matches actual package

License

Notifications You must be signed in to change notification settings

timdrysdale/ifcmp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ifcmp

Compare two golang interface definitions, checking the version in README matches that in the code.

Usage

ifcmp <README.md> <interface.go> <interface>

Why does this tool exist?

Strictly speaking, this interface comparison tool isn't really needed because a potential library user can always check the exact details of methods etc in the package listing hosted at pkg.go.dev. But a project I contributed to recently had for convenience included the interface methods in the README, with lossy comments marking sections and documentation comments removed. It would be useful to automatically check for consistency with the code. Automation could take several forms, in increasing order of effort required:

  • remove section and rely on pkg.go.dev automatic documentation
  • regexp to identify methods, which are then mapped, treating the remainder of the line as the map value (with some whitespace sanitisation)
  • compare the Abstract Syntax Trees generated by parsing the README.md code section and the actual code

I wanted to learn more about the type system in go, to prepare for an upcoming project that will be receiving dynamically-generated JSON objects. They weren't availalble yet, but abstract syntax trees (AST) from the go parser were. So out with the sledgehammer to crack a nut. There's a nice introduction to traversing AST here.

Method

Use Go's parser to create an AST from the actual code, and another from the interface code in the README. Extract the list of functions, their parameters and returns, and compare them (ignore the details of whitespace, comments and method order, but preserve parameter and result orders).

Cleaning up the README.md for parsing

The README.md is searched for code blocks containing type <interface> interface, which are the only contents from the README which are presented to the parser.

What's checked?

The library I was checking contained methods with parameters and results of the following types:

*ast.Ident:
*ast.InterfaceType:
*ast.ArrayType:
*ast.StarExpr:
*ast.SelectorExpr:

The last three are recursive.

A number of other types are specified in ast, and it is either unlikely or impossible that they show up in a valid FuncType so are not currently supported.

What's in the AST?

There's a nice introduction to traversing AST here. A list of AST tips indicates node replacement is possible.

Applying the basic approach to our example in ./testdata we see many lines of output when we print the whole tree.

Using the ast.Print() method, you can see the GoCloak interface as an ast.GenDecl (line 39), named on line 47, and identified as an *ast.InterfaceType on line 55.

<snip>
    39  .  .  1: *ast.GenDecl {
    40  .  .  .  TokPos: ./testdata/gocloak.go:11:1
    41  .  .  .  Tok: type
    42  .  .  .  Lparen: -
    43  .  .  .  Specs: []ast.Spec (len = 1) {
    44  .  .  .  .  0: *ast.TypeSpec {
    45  .  .  .  .  .  Name: *ast.Ident {
    46  .  .  .  .  .  .  NamePos: ./testdata/gocloak.go:11:6
    47  .  .  .  .  .  .  Name: "GoCloak"
    48  .  .  .  .  .  .  Obj: *ast.Object {
    49  .  .  .  .  .  .  .  Kind: type
    50  .  .  .  .  .  .  .  Name: "GoCloak"
    51  .  .  .  .  .  .  .  Decl: *(obj @ 44)
    52  .  .  .  .  .  .  }
    53  .  .  .  .  .  }
    54  .  .  .  .  .  Assign: -
    55  .  .  .  .  .  Type: *ast.InterfaceType {
    56  .  .  .  .  .  .  Interface: ./testdata/gocloak.go:11:14
    57  .  .  .  .  .  .  Methods: *ast.FieldList {
    58  .  .  .  .  .  .  .  Opening: ./testdata/gocloak.go:11:24
    59  .  .  .  .  .  .  .  List: []*ast.Field (len = 189) {
    60  .  .  .  .  .  .  .  .  0: *ast.Field {
    61  .  .  .  .  .  .  .  .  .  Names: []*ast.Ident (len = 1) {
    62  .  .  .  .  .  .  .  .  .  .  0: *ast.Ident {
    63  .  .  .  .  .  .  .  .  .  .  .  NamePos: ./testdata/gocloak.go:13:2
    64  .  .  .  .  .  .  .  .  .  .  .  Name: "RestyClient"
    65  .  .  .  .  .  .  .  .  .  .  .  Obj: *ast.Object {
    66  .  .  .  .  .  .  .  .  .  .  .  .  Kind: func
    67  .  .  .  .  .  .  .  .  .  .  .  .  Name: "RestyClient"
    68  .  .  .  .  .  .  .  .  .  .  .  .  Decl: *(obj @ 60)
    69  .  .  .  .  .  .  .  .  .  .  .  }
    70  .  .  .  .  .  .  .  .  .  .  }
    71  .  .  .  .  .  .  .  .  .  }
 <snip>

Looking in go/ast/ast.go, we find:

type File struct {
  	Doc        *CommentGroup   // associated documentation; or nil
  	Package    token.Pos       // position of "package" keyword
  	Name       *Ident          // package name
  	Decls      []Decl          // top-level declarations; or nil
  	Scope      *Scope          // package scope (this file only)
  	Imports    []*ImportSpec   // imports in this file
  	Unresolved []*Ident        // unresolved identifiers in this file
  	Comments   []*CommentGroup // list of all comments in the source file
}

The Name field is the package name, not the interface name, so we cannot use that.

in go/ast/scope.go we see

type Scope struct {
    20  	Outer   *Scope
    21  	Objects map[string]*Object
    22  }

We're looking here for a scope of Type, with the interface name.

The Objects map uses the object name as the key (which will be the interface name when the object is the interface)

type Object struct {
   	Kind ObjKind
   	Name string      // declared name
   	Decl interface{} // corresponding Field, XxxSpec, FuncDecl, LabeledStmt, AssignStmt, Scope; or nil
   	Data interface{} // object-specific data; or nil
   	Type interface{} // placeholder for type information; may be nil
   }

// ObjKind describes what an object represents.
  type ObjKind int
  
  // The list of possible Object kinds.
  const (
  	Bad ObjKind = iota // for error handling
  	Pkg                // package
  	Con                // constant
  	Typ                // type
  	Var                // variable
  	Fun                // function or method
  	Lbl                // label
  )

The Decl field for an interface is of type *ast.TypeSpec

// All expression nodes implement the Expr interface.
  type Expr interface {
  	Node
  	exprNode()
  }

The methods are all of FuncType

// A FuncType node represents a function type.
  	FuncType struct {
  		Func    token.Pos  // position of "func" keyword (token.NoPos if there is no "func")
  		Params  *FieldList // (incoming) parameters; non-nil
  		Results *FieldList // (outgoing) results; or nil
  	}

Parameters and results are stored in FieldList

// A FieldList represents a list of Fields, enclosed by parentheses or braces.
    type FieldList struct {
    	Opening token.Pos // position of opening parenthesis/brace, if any
    	List    []*Field  // field list; or nil
    	Closing token.Pos // position of closing parenthesis/brace, if any
    }

Which have arrays of Field

//A Field represents a Field declaration list in a struct type 
<snip>
type Field struct {
  	Doc     *CommentGroup // associated documentation; or nil
  	Names   []*Ident      // field/method/parameter names; or nil
  	Type    Expr          // field/method/parameter type
  	Tag     *BasicLit     // field tag; or nil
  	Comment *CommentGroup // line comments; or nil
  }

Note that Expr can themselves contain Expr, so the function to generate the string representation is recursive.

Example results from run on live repo

Actual: GetResource(ctx context.Context, token, realm, clientID, resourceID string) (*ResourceRepresentation, error)
Readme: GetResource(ctx context.Context, token, realm, clientID, resourceID string) (*Resource, error)

Actual: UpdateResource(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) error
Readme: UpdateResource(ctx context.Context, token, realm, clientID string, resource Resource) error

Actual: DecodeAccessTokenCustomClaims(ctx context.Context, accessToken, realm, expectedAudience string, claims jwt.Claims) (*jwt.Token, error)
Readme: DecodeAccessTokenCustomClaims(ctx context.Context, accessToken, realm string, claims jwt.Claims) (*jwt.Token, error)

Actual: CreateRealmRole(ctx context.Context, token, realm string, role Role) (string, error)
Readme: CreateRealmRole(ctx context.Context, token, realm string, role Role) error

Actual: GetResources(ctx context.Context, token, realm, clientID string, params GetResourceParams) ([]*ResourceRepresentation, error)
Readme: GetResources(ctx context.Context, token, realm, clientID string) ([]*Resource, error)

Actual: CreateResource(ctx context.Context, token, realm, clientID string, resource ResourceRepresentation) (*ResourceRepresentation, error)
Readme: CreateResource(ctx context.Context, token, realm, clientID string, resource Resource) (*Resource, error)

Actual: CreateGroup(ctx context.Context, accessToken, realm string, group Group) (string, error)
Readme: CreateGroup(ctx context.Context, accessToken, realm string, group Group) error

Actual: CreateClient(ctx context.Context, accessToken, realm string, clientID Client) (string, error)
Readme: CreateClient(ctx context.Context, accessToken, realm string, clientID Client) error

Actual: CreateClientProtocolMapper(ctx context.Context, token, realm, clientID string, mapper ProtocolMapperRepresentation) (string, error)
Readme: CreateClientProtocolMapper(ctx context.Context, token, realm, clientID string, mapper ProtocolMapperRepresentation) error

Actual: CreateComponent(ctx context.Context, accessToken, realm string, component Component) (string, error)
Readme: CreateComponent(ctx context.Context, accessToken string, realm, component Component) error

Actual: LoginClientSignedJWT(ctx context.Context, clientID, realm string, key interface{}, signedMethod jwt.SigningMethod, expiresAt *jwt.Time) (*JWT, error)
Readme: LoginClientSignedJWT(ctx context.Context, clientID, realm string, key interface{}, signedMethod jwt.SigningMethod, expiresAt int64) (*JWT, error)

Actual: CreateClientRole(ctx context.Context, accessToken, realm, clientID string, role Role) (string, error)
Readme: CreateClientRole(ctx context.Context, accessToken, realm, clientID string, role Role) error

Actual: DecodeAccessToken(ctx context.Context, accessToken, realm, expectedAudience string) (*jwt.Token, *jwt.MapClaims, error)
Readme: DecodeAccessToken(ctx context.Context, accessToken, realm string) (*jwt.Token, *jwt.MapClaims, error)

Actual: DeletePolicy(ctx context.Context, token, realm, clientID, policyID string) error
Readme: DeletePolicy(ctx context.Context, token, realm, clientID string, policyID string) error

Actual: CreateClientScope(ctx context.Context, accessToken, realm string, scope ClientScope) (string, error)
Readme: CreateClientScope(ctx context.Context, accessToken, realm string, scope ClientScope) error

About

Compare two golang interface definitions, e.g. check version in README matches actual package

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published