Ejemplo n.º 1
0
func New() *importsTenet {
	t := &importsTenet{}
	t.SetInfo(tenet.Info{
		Name:        "imports",
		Usage:       "police the imports of a package",
		Description: "imports matching the following regex are restricted: \"{{.blacklist_regex}}\"",
		SearchTags:  []string{"import"},
		Language:    "go",
	})

	blacklisted := t.RegisterOption("blacklist_regex", "", "a regex to filter imports against")
	issue := t.RegisterIssue("blacklisted_import",
		tenet.AddComment(`This package should not be bringing in {{.importName}}`),
	)
	// Metrics and tags could also be registered at this point to help track
	// this type of issue.

	t.SmellNode(func(r tenet.Review, imp *ast.ImportSpec) error {
		importName := imp.Path.Value
		m, err := regexp.MatchString(*blacklisted, importName)
		if err != nil {
			return errors.Trace(err)
		}

		if m {
			r.RaiseNodeIssue(issue, imp, tenet.CommentVar("importName", importName))
		}
		return nil
	})

	return t
}
Ejemplo n.º 2
0
func New() *unusedArgTenet {
	t := &unusedArgTenet{}
	t.SetInfo(tenet.Info{
		Name:        "unused_arg",
		Usage:       "catch funcs that don't use an argument",
		Description: "Ensure a function's arguments are used in the body of the function.",
		SearchTags:  []string{"function", "method"},
		Language:    "golang",
	})

	confidence := t.RegisterMetric("confidence")
	issue := t.RegisterIssue("unused_func_arg",
		// TODO(waigani) "Argument{s} {args} {is_are} not used in the function's body."
		tenet.AddComment(`{{.args}} used. If this is intentional, please change the name to "_".`, tenet.FirstComment),
		tenet.AddComment(`{{.args}} used. Please remove or set the name to "_".`, tenet.SecondComment),
		tenet.AddComment(`{{.args}} used. Set to "_" or remove.`, tenet.ThirdComment),
		tenet.AddComment(`{{.args}} used. You get the idea ...`, tenet.FourthComment),
	)

	t.SmellNode(func(r tenet.Review, fnc *ast.FuncDecl) error {
		args := fnc.Type.Params.List
		if len(args) == 0 {
			return nil
		}

		v := &visitor{
			args: map[string]bool{},
		}
		for _, arg := range args {
			for _, ident := range arg.Names {
				n := ident.Name
				if n != "" && n != "_" {
					v.args[n] = true
				}
			}
		}
		ast.Walk(v, fnc.Body)
		if len(v.args) > 0 {
			names := make([]string, 0, len(v.args))
			for k := range v.args {
				names = append(names, k)
			}
			unused := `"` + strings.Join(names, `", "`)

			if len(v.args) > 1 {
				unused += `" aren't`
			} else {
				unused += `" isn't`
			}

			r.RaiseNodeIssue(issue, fnc, confidence(0.5), tenet.CommentVar("args", unused))
		}

		return nil
	})
	return t
}
Ejemplo n.º 3
0
func New() *licenseTenet {
	t := &licenseTenet{}
	t.SetInfo(tenet.Info{
		Name:        "license",
		Usage:       "Each file should contain the appropriate license header.",
		Description: "Ensure that each file in the project begins with a license: \"{{.header}}\"",
		SearchTags:  []string{"license", "comment", "doc-comment"},
		Language:    "go",
	})

	confidence := t.RegisterMetric("confidence")
	issue := t.RegisterIssue("incorrect_header", tenet.AddComment(`
Each file should start with a license header of the following format:

{{.header}}
`[1:], tenet.FirstComment),
		tenet.AddComment("This file also needs the correctly formatted license header.", tenet.SecondComment),
		tenet.AddComment("And so on, license header again.", tenet.ThirdComment),
	)

	// RegisterOption returns a pointer to a string. The string will be
	// updated by the time it is used in the smell.
	headerPnt := t.RegisterOption("header", "Copyright Me", "the license header to check for")

	t.SmellLine(func(r tenet.Review, n int, line []byte) error {
		headerVal := *headerPnt
		lineMatchesHeader := func() bool {
			var i int
			header := strings.Split(headerVal, "\n")
			for i = 1; i <= len(header); i++ {
				if n == i && string(line) != header[i-1] {
					// It doesn't match, no need to keep sniffing this file.
					r.SmellDoneWithFile()
					return false
				}
			}
			if n == i {
				fmt.Println(n, i, string(line), headerVal)
				// Every line of the header matches. Don't check again.
				r.SmellDoneWithFile()
			}
			return true
		}

		if !lineMatchesHeader() {
			r.RaiseLineIssue(issue, 1, 1, confidence(0.7), tenet.CommentVar("header", headerVal))
		}

		return nil
	})

	return t
}
Ejemplo n.º 4
0
func New() *assertLoopLenTenet {
	t := &assertLoopLenTenet{}
	t.SetInfo(tenet.Info{
		Name:        "juju_test_assert_loop_len",
		Usage:       "If asserting within a loop, the length of the colleciton being iterated should be asserted",
		Description: "If asserting within a loop, the length of the colleciton being iterated should be asserted",
		SearchTags:  []string{"test", "loop"},
		Language:    "go",
	})

	assertLoopIssue := t.RegisterIssue("loop_len_not_asserted",
		tenet.AddComment(`The asserts within this loop may never get run. The length of "{{.looped}}" needs to be asserted.`, tenet.FirstComment),
		tenet.AddComment(`The length of "{{.looped}}" needs to be asserted also`, tenet.DefaultComment),
	)

	// TODO(waigani) good canditate for a patch fix.
	rangeCallExpIssue := t.RegisterIssue("range_over_call_exp",
		tenet.AddComment(`
Even if you assert the length of the result of this call before iterating
over it, you cannot guarantee the result will be the same each time you call
it. You cannot be sure that the asserts within the for loop will get
run. Please assign the result of {{.looped}} to a variable, assert the expected
length of the variable and then loop over that.`[1:], tenet.FirstComment),
		tenet.AddComment(`
Here also, you can't gurantee the length of {{.looped}}'s result.`[1:], tenet.SecondComment),
		tenet.AddComment(`
Again, need to assert result of {{.looped}} first.`[1:], tenet.DefaultComment),
	)

	// // First, knock out any file that isn't a test
	t.SmellNode(func(r tenet.Review, _ *ast.File) error {
		if !strings.HasSuffix(r.File().Filename(), "_test.go") {
			r.FileDone()
		}
		return nil
	})

	// All nodes that have been asserted in a loop.
	var ranged possibleBadRange

	// Check if any range body contains an assert or check.
	t.SmellNode(func(r tenet.Review, rngStmt *ast.RangeStmt) error {

		// TODO(waigani) need to return inc statements and check they are
		// asserted after loop.
		if containsIncStmt(rngStmt.Body.List) {
			return nil
		}

		if containsCheckOrAssert(rngStmt.Body.List) {
			switch n := rngStmt.X.(type) {
			case *ast.CallExpr:
				looped := "the call"
				switch x := n.Fun.(type) {
				case *ast.Ident:
					looped = x.Name + "()"
				case *ast.SelectorExpr:
					looped = x.Sel.Name + "()"
				}
				r.RaiseNodeIssue(rangeCallExpIssue, n, tenet.CommentVar("looped", looped))
			case *ast.Ident:
				ranged.add(n)
			default:
				// TODO(waigani) log unknown range symbol
			}
		}
		return nil
	})

	// Find any idents that have been constructed in this file.
	t.SmellNode(func(r tenet.Review, ident *ast.Ident) error {

		if ranged.empty() {
			// Nothing was found to be ranged over. No need to keep smelling.
			r.FileDone()
		}

		// This ident has not been ranged over, so we're not interested in it.
		if !ranged.has(ident) {
			return nil
		}

		isCompLit, err := astutil.DeclaredWithLit(ident)
		if err != nil {
			return errors.Trace(err)
		}

		if isCompLit {
			// The var has been constructed in this file, so it is clear
			// to see it's length. We are no longer interested in it.
			ranged.remove(ident)
		}

		return nil
	})

	// Check that any remaining ranged vars not constructed in this file have had their lengths asserted.
	t.SmellNode(func(r tenet.Review, callExpr *ast.CallExpr) error {
		if fun, ok := callExpr.Fun.(*ast.SelectorExpr); ok {
			if fun.Sel.String() != "Assert" && fun.Sel.String() != "Check" {
				return nil
			}
			if args := callExpr.Args; len(args) == 3 {

				switch n := args[0].(type) {
				case *ast.Ident:

					// we have an assert on the collection.
					if sel, ok := args[1].(*ast.SelectorExpr); ok {
						if sel.Sel.String() == "HasLen" {

							// The length has been asserted, wer're no longer
							// interested in this var. Remove it if we added it.
							ranged.remove(n)
							return nil
						}
					}
				case *ast.CallExpr:
					if f, ok := n.Fun.(*ast.Ident); ok {
						if f.Name == "len" {
							if x, ok := n.Args[0].(*ast.Ident); ok {
								// The length has been asserted, wer're no longer
								// interested in this var. Remove it if we added it.
								ranged.remove(x)
							}
						}
					}
				}

			}
		}
		return nil
	})

	// Do one more run over range statements.
	t.SmellNode(func(r tenet.Review, rngStmt *ast.RangeStmt) error {

		if ranged.empty() {
			// There are no possible vars of unasserted len in this file.
			// Don't run any more smells.
			r.FileDone()
		}

		// Find our assert ranges again
		if containsCheckOrAssert(rngStmt.Body.List) {
			switch n := rngStmt.X.(type) {
			case *ast.Ident:
				// we're only interested in idents this time
				if ranged.has(n) {
					r.RaiseNodeIssue(assertLoopIssue, n, tenet.CommentVar("looped", n.Name))
				}
			}
		}
		return nil
	})

	return t
}
Ejemplo n.º 5
0
func New() *unusedArgTenet {
	t := &unusedArgTenet{}
	t.SetInfo(tenet.Info{
		Name:        "exhaustive_switch",
		Usage:       "ensure that a switch has a case for every possible value of a variable",
		Description: "Ensure that a switch has a case for every possible value of a variable",
		SearchTags:  []string{"switch"},
		Language:    "golang",
	})

	exhaustTag := t.RegisterOption("exhaust_tag", "lingo:exhaustive", "If this tag is found in a comment above a switch statement, that switch will be treated as an exhaustive switch.")
	issue := t.RegisterIssue("missing_case",
		tenet.AddComment(`The following cases are missing from this switch:{{.cases}}.`),
	)

	// map of a switched variable to all case values.
	switchedVarSets := map[int]map[string][]ast.Expr{}

	t.SmellNode(func(r tenet.Review, file *ast.File) error {

		for _, com := range file.Comments {
			if strings.Contains(com.Text(), *exhaustTag) {
				switchedVarSets[r.File().Fset().Position(com.End()).Line+1] = map[string][]ast.Expr{}
			}
		}

		return nil
	})

	// First find switched vars
	t.SmellNode(func(r tenet.Review, swt *ast.SwitchStmt) error {

		// Did we find a tag for this switch?
		swtLine := r.File().Fset().Position(swt.Pos()).Line
		if _, ok := switchedVarSets[swtLine]; !ok {
			return nil
		}

		var switchedVar string
		if ident, ok := swt.Tag.(*ast.Ident); ok {

			var err error
			switchedVar, err = astutil.TypeOf(ident)
			if err != nil {
				return errors.Trace(err)
			}
		} else {
			return errors.Errorf("found switch, but could not get variable name: %#v", swt.Tag)
		}

		for _, stm := range swt.Body.List {
			if c, ok := stm.(*ast.CaseClause); ok {
				for _, l := range c.List {

					switchedVarSets[swtLine][switchedVar] = append(switchedVarSets[swtLine][switchedVar], l)
				}
			}
		}

		return nil
	})

	missingCases := map[int][]*ast.Ident{}

	// Then find genDecls with the switched type in it and make sure all
	// GenDecls of that type are switched on.
	t.SmellNode(func(r tenet.Review, genDec *ast.GenDecl) error {

		if len(switchedVarSets) == 0 {
			r.FileDone()
		}

		for switchLine, switchSet := range switchedVarSets {
			for switchType := range switchSet {

				var inGenDecl bool
				// first see if a switched type is in this genDecl
				for _, s := range genDec.Specs {

					if valSpec, ok := s.(*ast.ValueSpec); ok {

						for _, n := range valSpec.Names {

							typ, err := astutil.TypeOf(n)
							if err != nil {
								// TODO(waigani) log
								continue
							}

							// Does the type in this GenDecl match that of the
							// switch?
							if typ == switchType {
								inGenDecl = true
							}

						}

					}
				}

				if !inGenDecl {
					continue
				}

				// Now make sure there is a case for each GenGecl
				for _, s := range genDec.Specs {
					if valSpec, ok := s.(*ast.ValueSpec); ok {
						for _, vName := range valSpec.Names {

							// Does our switch contain the var from  genDecl?
							if !contains(vName, switchedVarSets[switchLine][switchType]) {
								missingCases[switchLine] = append(missingCases[switchLine], vName)
							}
						}
					}
				}
			}
		}

		return nil
	})

	// Do another sweep over switches, this time raising an issue for any in the missingCases map
	t.SmellNode(func(r tenet.Review, swt *ast.SwitchStmt) error {

		if len(missingCases) == 0 {
			// no switches with missing cases were found. Don't smell any more
			// nodes.
			r.SmellDone()
			return nil
		}

		switchLine := r.File().Fset().Position(swt.Pos()).Line

		if cases, ok := missingCases[switchLine]; ok {
			var missing string
			for _, m := range cases {
				missing += ", " + m.String()
			}

			r.RaiseNodeIssue(issue, swt, tenet.CommentVar("cases", missing[1:]))
		}
		return nil
	})

	return t
}