Skip to content

Commit

Permalink
adding custom linters to the code base for static analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
christopher-henderson committed Feb 9, 2021
1 parent f091dd3 commit a575e45
Show file tree
Hide file tree
Showing 19 changed files with 677 additions and 24 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ jobs:
- name: Run integration tests
run: make integration PARALLELISM=3
working-directory: v3

- name: Run custom code linters
run: make custom-code-lint
working-directory: v3
31 changes: 31 additions & 0 deletions v3/integration/lints/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Linting the Linter

This directory contains a collection of Golang code linters that are intended to be very specific to ZLint itself.

# Running

```bash
go run main.go <path to code directory>
```

The linter will walk the given directory recursively and attempt to parse and lint each Go file it comes accross.

In order to extend this custom linter, write a new Go file in the `lints` directory which contains a struct that implements the following interface.

# Extending

```go
type Lint interface {
Lint(tree *ast.File, file *File) *Result
CheckApplies(tree *ast.File, file *File) bool
}
```

Then go in to `main.go` and add a pointer to your lint to the `Linters` slice.

```go
var Linters = []lint.Lint{
&lints.InitFirst{},
&lints.MySuperCoolLint{}
}
```
33 changes: 33 additions & 0 deletions v3/integration/lints/filters/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package filters

/*
* ZLint Copyright 2021 Regents of the University of Michigan
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import (
"strings"

"github.com/zmap/zlint/v3/integration/lints/lint"
)

func IsALint(file *lint.File) bool {
return strings.HasPrefix(file.Name, "lint_") && IsAGoFile(file) && !IsATest(file)
}

func IsAGoFile(file *lint.File) bool {
return strings.HasSuffix(file.Name, ".go")
}

func IsATest(file *lint.File) bool {
return strings.HasSuffix(file.Name, "test.go")
}
46 changes: 46 additions & 0 deletions v3/integration/lints/filters/nodes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package filters

import "go/ast"

// Declarations takes in a list of a declarations and a predicate of that takes in one declaration
// and returns a boolean. Only the declarations for which `predicate` returns true will be included in
// the returned list of declarations.
//
// For example, the following returns a list of only function declarations.
//
// filters.Declarations(tree.Decls, func(decl ast.Decl) bool {
// _, ok := decl.(*ast.FuncDecl)
// return ok
// })
//
// The order of declarations is maintained.
func Declarations(decls []ast.Decl, predicate func(decl ast.Decl) bool) (filtered []ast.Decl) {
for _, decl := range decls {
if predicate(decl) {
filtered = append(filtered, decl)
}
}
return
}

// FunctionsOnly returns a list of only the most outer function declarations present within
// the provided list. This filter does NOT recurse into those function declarations to find lambdas.
// For example, the following file...
//
// func hi() bool {
// return func() bool {
// return true
// }()
// }
//
// func hello() bool {
// return false
// }
//
// ...will return the hi and hello functions but on the inner lambda within hi.
func FunctionsOnly(decls []ast.Decl) []ast.Decl {
return Declarations(decls, func(decl ast.Decl) bool {
_, ok := decl.(*ast.FuncDecl)
return ok
})
}
199 changes: 199 additions & 0 deletions v3/integration/lints/lint/lint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package lint

/*
* ZLint Copyright 2021 Regents of the University of Michigan
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"path/filepath"
"strings"
)

type Lint interface {
Lint(tree *ast.File, file *File) *Result
CheckApplies(tree *ast.File, file *File) bool
}

// A Result encodes any unmet expectation laid out by your lint. It consists of a single message, a list of code
// citations, and a list of lint citations.
//
// The message should be succinct and descriptive of the core issue. This message can only be set in the constructor,
// NewResult. For example...
//
// "Go style guides suggest not using bare returns in complex functions"
//
// Code citations are the locations within the file that did not meet your expectations. Please see AddCodeCitations
// for information on how to add these to the Result type. Adding a code citation will result in the file, line number
// and raw source code appearing in the lint result. For example...
//
// File ../../lints/cabf_br/lint_cab_dv_conflicts_with_locality.go, line 28
//
// func (l *certPolicyConflictsWithLocality) Initialize() error {
// return nil
// }
//
// The lint citations are additional information to help the contributor understand why their code failed
// this lint and, if possible, some hints or resources on how to correct the issue. Every citation will listed on its
// own line.
type Result struct {
message string
codeCitations []string
citations []string
}

func NewResult(message string) *Result {
return &Result{message: message}
}

// AddCodeCitation takes the starting ending position of a block of code within a file.
// Upon calling the String method, every code citation will be printed alongside the
// result. This code citation listed the file and line of the code in question
// as well as the raw block of source code.
//
// For example:
//
// File ../../lints/cabf_br/lint_cab_dv_conflicts_with_locality.go, line 28
//
// func (l *certPolicyConflictsWithLocality) Initialize() error {
// return nil
// }
//
//
func (r *Result) AddCodeCitation(start, end token.Pos, file *File) *Result {
srcCode := make([]byte, end-start)
reader := strings.NewReader(file.Src)
// We have no real interest in the error return since this is an in-memory reader.
_, _ = reader.ReadAt(srcCode, int64(start))
lineno := file.LineOf(start)
citation := fmt.Sprintf("File %s, line %d\n\n%s\n\n", file.Path, lineno, string(srcCode))
r.codeCitations = append(r.codeCitations, citation)
return r
}

// SetCitations sets a list of citations that users can reference in order to understand
// the error that they received. Upon calling the String method each citation will be
// listed on their on own line.
//
// For example:
//
// For more information, please see the following citations.
// https://github.com/zmap/zlint/issues/371
// https://golang.org/doc/effective_go.html#init
//
// The above links a GitHub issue that discuss the lint in question as well as a link
// to Golang's magic `init` method (because the lint in question is ask the contributor
// to implement `init` at a particular spot in the file).
func (r *Result) SetCitations(citations ...string) *Result {
r.citations = citations
return r
}

func (r *Result) String() string {
b := strings.Builder{}
b.WriteString("--------------------\n")
b.WriteString("Linting Error\n\n")
b.WriteString(r.message)
b.WriteString("\n\n")
for _, code := range r.codeCitations {
b.WriteString(code)
}
if len(r.citations) > 0 {
b.WriteString("For more information, please see the following citations.\n")
}
for _, citation := range r.citations {
b.WriteByte('\t')
b.WriteString(citation)
b.WriteByte('\n')
}
return b.String()
}

type File struct {
Src string
Path string
Name string
Lines []string
}

// LineOf computes which line a particular position within a file lands on.
//
// This is not the greatest song in the world.
// No, this is just a tribute.
// Couldn't remember the greatest song in the world.
// No, this is just a tribute!
//
// The word "remember" begins at position 81 within this text, therefor LineOf(81) should return line 3.
func (f *File) LineOf(pos token.Pos) int {
start := 0
end := 0
for lineno, line := range f.Lines {
start = end
end = start + len(line)
if int(pos) >= start && int(pos) <= end {
return lineno + 1
}
}
return int(token.NoPos)
}

func NewFile(name, src string) *File {
return &File{src, name, filepath.Base(name), strings.Split(src, "\n")}
}

func Parse(path string) (*ast.File, *File, error) {
fset := new(token.FileSet)
tree, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return nil, nil, err
}
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, nil, err
}
file := NewFile(path, string(b))
return tree, file, nil
}

func RunLintForFile(path string, lint Lint) (*Result, error) {
tree, file, err := Parse(path)
if err != nil {
return nil, err
}
return RunLint(tree, file, lint), nil
}

func RunLint(tree *ast.File, file *File, lint Lint) *Result {
if !lint.CheckApplies(tree, file) {
return nil
}
return lint.Lint(tree, file)
}

func RunLints(path string, lints []Lint) ([]*Result, error) {
tree, file, err := Parse(path)
if err != nil {
return nil, err
}
var results []*Result
for _, lint := range lints {
if result := RunLint(tree, file, lint); result != nil {
results = append(results, result)
}
}
return results, nil
}
57 changes: 57 additions & 0 deletions v3/integration/lints/lints/init_first.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package lints

/*
* ZLint Copyright 2021 Regents of the University of Michigan
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import (
"go/ast"
"go/token"

"github.com/zmap/zlint/v3/integration/lints/filters"
"github.com/zmap/zlint/v3/integration/lints/lint"
)

type InitFirst struct{}

func (i *InitFirst) CheckApplies(tree *ast.File, file *lint.File) bool {
return filters.IsALint(file)
}

func (i *InitFirst) Lint(tree *ast.File, file *lint.File) *lint.Result {
functions := filters.FunctionsOnly(tree.Decls)
if len(functions) == 0 {
return lint.NewResult("Lint does not contain any functions or methods").
AddCodeCitation(token.NoPos, token.NoPos, file)
}
// filters.FunctionsOnly have given us some guarantee that this type cast will succeed.
firstFunction := functions[0].(*ast.FuncDecl)
if isInit(firstFunction) {
return nil
}
return lint.NewResult("Got the wrong method signature as the first function declaration within the linter.\n"+
"ZLint lints must have func init() { ... } as their first function declaration").
AddCodeCitation(firstFunction.Pos(), firstFunction.End(), file).
SetCitations(
"https://github.com/zmap/zlint/issues/371",
"https://golang.org/doc/effective_go.html#init",
)
}

func isInit(function *ast.FuncDecl) bool {
isNamedInit := function.Name.Name == "init"
isNotAMethod := function.Recv == nil
hasNoParameters := len(function.Type.Params.List) == 0
hasNoReturns := function.Type.Results == nil
return isNamedInit && isNotAMethod && hasNoParameters && hasNoReturns
}
Loading

0 comments on commit a575e45

Please sign in to comment.