github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/cmd/tast-lint/internal/check/tast_search_flags.go (about)

     1  // Copyright 2022 The ChromiumOS Authors
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package check
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/token"
    11  	"strings"
    12  )
    13  
    14  // hasImport checks if the import declaration contains specified package name.
    15  // If so, it returns the name of the import, and an empty string otherwise.
    16  func hasImport(f *ast.File, pkgName string) string {
    17  	sfmt := fmt.Sprintf("\"%s\"", pkgName)
    18  	for _, decl := range f.Decls {
    19  		genDecl, ok := decl.(*ast.GenDecl)
    20  		if !ok || genDecl.Tok != token.IMPORT {
    21  			continue
    22  		}
    23  
    24  		for _, spec := range genDecl.Specs {
    25  			importSpec, ok := spec.(*ast.ImportSpec)
    26  			if !ok || importSpec.Path.Kind != token.STRING {
    27  				continue
    28  			}
    29  
    30  			if importSpec.Path.Value == sfmt {
    31  				if importSpec.Name != nil {
    32  					return importSpec.Name.Name
    33  				}
    34  
    35  				sparts := strings.Split(pkgName, "/")
    36  				return sparts[len(sparts)-1]
    37  			}
    38  		}
    39  
    40  		break
    41  	}
    42  
    43  	return ""
    44  }
    45  
    46  // hasTastTestInit checks if the given function declaration is an init function
    47  // which declares a Tast test. If so, it returns true, and false otherwise.
    48  func hasTastTestInit(f ast.FuncDecl, testingPkgName string) bool {
    49  	if f.Name.Name != "init" || len(f.Body.List) != 1 {
    50  		return false
    51  	}
    52  
    53  	exprStmt, ok := f.Body.List[0].(*ast.ExprStmt)
    54  	if !ok {
    55  		return false
    56  	}
    57  
    58  	callExpr, ok := exprStmt.X.(*ast.CallExpr)
    59  	if !ok {
    60  		return false
    61  	}
    62  
    63  	selectorExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
    64  	if !ok || selectorExpr.Sel.Name != "AddTest" {
    65  		return false
    66  	}
    67  
    68  	ident, ok := selectorExpr.X.(*ast.Ident)
    69  	if !ok || ident.Name != testingPkgName {
    70  		return false
    71  	}
    72  
    73  	return true
    74  }
    75  
    76  // extractFunctions extracts all top-level function declarations from the given
    77  // File. It returns a map where the key is the name of the function, and the
    78  // value is the function declaration.
    79  func extractFunctions(f *ast.File) map[string]ast.FuncDecl {
    80  	m := make(map[string]ast.FuncDecl)
    81  	for _, decl := range f.Decls {
    82  		funcDecl, ok := decl.(*ast.FuncDecl)
    83  		if !ok {
    84  			continue
    85  		}
    86  
    87  		m[funcDecl.Name.Name] = *funcDecl
    88  	}
    89  
    90  	return m
    91  }
    92  
    93  // extractValue tries to extract the value from a SelectorExpr, or a string
    94  // BasicLit. If successful, it returns the string value and the position of the
    95  // token. Otherwise, it returns an empty string and token.NoPos.
    96  // When the allowedValues is not nil, it returns only the values that are keys
    97  // in the map, and that have a value of true.
    98  func extractValue(node ast.Node, allowedValues map[string]bool, pkgName string) (string, token.Pos) {
    99  	var value string
   100  
   101  	// Check if the value is defined as a selector expression.
   102  	selectorExpr, ok := node.(*ast.SelectorExpr)
   103  	if ok {
   104  		ident, ok := selectorExpr.X.(*ast.Ident)
   105  		if ok && ident.Name == pkgName {
   106  			value = selectorExpr.Sel.Name
   107  		}
   108  	}
   109  
   110  	// Check if the value is defined as a string literal.
   111  	basicLit, ok := node.(*ast.BasicLit)
   112  	if ok && basicLit.Kind == token.STRING && len(basicLit.Value) > 2 {
   113  		value = basicLit.Value[1 : len(basicLit.Value)-1]
   114  	}
   115  
   116  	if allowedValues != nil {
   117  		val, ok := allowedValues[value]
   118  		if !ok || !val {
   119  			return "", token.NoPos
   120  		}
   121  	}
   122  
   123  	return value, node.Pos()
   124  }
   125  
   126  // extractValues tries to extract the value from the given nodes.
   127  // To decide which values to select, extractValue is used.
   128  func extractValues(nodes []ast.Node, allowedValues map[string]bool, pkgName string) map[string]token.Pos {
   129  	m := make(map[string]token.Pos)
   130  
   131  	// Find all Values in the Search Flags.
   132  	fv := funcVisitor(func(node ast.Node) {
   133  		value, pos := extractValue(node, allowedValues, pkgName)
   134  		if pos == token.NoPos {
   135  			return
   136  		}
   137  
   138  		m[value] = pos
   139  	})
   140  
   141  	for _, node := range nodes {
   142  		ast.Walk(fv, node)
   143  	}
   144  
   145  	return m
   146  }
   147  
   148  // extractSearchFlagNodes tries to extract the value from the SearchFlags and
   149  // ExtraSearchFlags Key-Value pairs. It returns a slice with the given nodes.
   150  func extractSearchFlagNodes(f *ast.File) []ast.Node {
   151  	var nodes []ast.Node
   152  
   153  	// Find all SearchFlag and ExtraSearchFlag declarations.
   154  	fv := funcVisitor(func(node ast.Node) {
   155  		kvExpr, ok := node.(*ast.KeyValueExpr)
   156  		if !ok {
   157  			return
   158  		}
   159  
   160  		keyIdent, ok := kvExpr.Key.(*ast.Ident)
   161  		if !ok || (keyIdent.Name != "SearchFlags" && keyIdent.Name != "ExtraSearchFlags") {
   162  			return
   163  		}
   164  
   165  		nodes = append(nodes, kvExpr.Value)
   166  	})
   167  
   168  	ast.Walk(fv, f)
   169  
   170  	return nodes
   171  }
   172  
   173  // extractSearchFlagValues tries to extract the value from the SearchFlags and
   174  // ExtraSearchFlags Key-Value pairs. It returns a slice with the given nodes.
   175  func extractSearchFlagValues(nodes []ast.Node, funcs map[string]ast.FuncDecl, allowedValues map[string]bool, pkgName string) (map[string]token.Pos, bool) {
   176  	m := make(map[string]token.Pos)
   177  
   178  	var nonFunctionNodes []ast.Node
   179  	for _, node := range nodes {
   180  		callExpr, ok := node.(*ast.CallExpr)
   181  		if !ok {
   182  			nonFunctionNodes = append(nonFunctionNodes, node)
   183  			continue
   184  		}
   185  
   186  		var name string
   187  		ident, ok := callExpr.Fun.(*ast.Ident)
   188  		if ok {
   189  			name = ident.Name
   190  		} else {
   191  			selectorExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
   192  			if !ok {
   193  				continue
   194  			}
   195  
   196  			ident, ok = selectorExpr.X.(*ast.Ident)
   197  			if !ok {
   198  				continue
   199  			}
   200  
   201  			name = fmt.Sprintf("%s.%s", ident.Name, selectorExpr.Sel.Name)
   202  		}
   203  
   204  		funcDecl, ok := funcs[name]
   205  		if !ok {
   206  			// We have an imported function.
   207  			return nil, true
   208  		}
   209  
   210  		// We have a function in the same file.
   211  		vals := extractValues([]ast.Node{funcDecl.Body}, allowedValues, pkgName)
   212  		m = union(m, vals)
   213  	}
   214  
   215  	vals := extractValues(nonFunctionNodes, allowedValues, pkgName)
   216  	m = union(m, vals)
   217  
   218  	return m, false
   219  }
   220  
   221  // extractTestFileValues tries to extract the value from the file based on the
   222  // extractValue function. If ignoredValues is not nil, the values that have the
   223  // specified position will be ignored.
   224  func extractTestFileValues(f *ast.File, values map[string]bool, pkgName string, ignoredValues map[string]token.Pos) map[string]token.Pos {
   225  	m := make(map[string]token.Pos)
   226  
   227  	// Find all Values in the file.
   228  	fv := funcVisitor(func(node ast.Node) {
   229  		value, pos := extractValue(node, values, pkgName)
   230  		if pos == token.NoPos {
   231  			return
   232  		}
   233  
   234  		if ignoredValues != nil {
   235  			ipos, ok := ignoredValues[value]
   236  			if ok && ipos == pos {
   237  				return
   238  			}
   239  		}
   240  
   241  		m[value] = pos
   242  	})
   243  
   244  	ast.Walk(fv, f)
   245  
   246  	return m
   247  }
   248  
   249  // SearchFlags checks search flags in Tast tests definitions for policy names
   250  // that have been used during the testing phase. If the file is not a Tast test,
   251  // then it is skipped.
   252  func SearchFlags(fs *token.FileSet, f *ast.File) (issues []*Issue) {
   253  	policyPkgName := hasImport(f, "go.chromium.org/tast-tests/cros/common/policy")
   254  	if policyPkgName == "" {
   255  		return
   256  	}
   257  
   258  	testingPkgName := hasImport(f, "go.chromium.org/tast/core/testing")
   259  	if testingPkgName == "" {
   260  		return
   261  	}
   262  
   263  	functions := extractFunctions(f)
   264  	if init, ok := functions["init"]; !ok || !hasTastTestInit(init, testingPkgName) {
   265  		return
   266  	}
   267  
   268  	policyNames := policyNames()
   269  	nodes := extractSearchFlagNodes(f)
   270  	tags, ok := extractSearchFlagValues(nodes, functions, policyNames, policyPkgName)
   271  	if ok {
   272  		return
   273  	}
   274  
   275  	usedPolicies := extractTestFileValues(f, policyNames, policyPkgName, tags)
   276  
   277  	for k, v := range usedPolicies {
   278  		_, ok := tags[k]
   279  		if ok {
   280  			continue
   281  		}
   282  
   283  		issues = append(issues, &Issue{
   284  			Pos:  fs.Position(v),
   285  			Msg:  fmt.Sprintf("Policy %s does not have a corresponding Search Flag.", k),
   286  			Link: "go/remote-management/tast-codelabs/policy_coverage_insights",
   287  		})
   288  	}
   289  
   290  	return
   291  }