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

     1  // Copyright 2018 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  	"bytes"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/format"
    12  	"go/parser"
    13  	"go/token"
    14  	"os"
    15  	"path"
    16  	"path/filepath"
    17  	"regexp"
    18  	"runtime"
    19  	"strconv"
    20  	"strings"
    21  )
    22  
    23  // entryPathRegexp matches a file name of an entry file.
    24  // verifier is a global singleton to check if AddTest() is used as designed.
    25  var tastTestsPattern = `/src/go\.chromium\.org/tast-tests/cros/(local|remote)/bundles/[^/]+/[^/]+/[^/]+\.go$`
    26  var tastTestsPrivatePattern = `/src/go\.chromium\.org/tast-tests-private/crosint/(local|remote)/bundles/[^/]+/[^/]+/[^/]+\.go$`
    27  
    28  var entryPathRegexp = regexp.MustCompile(fmt.Sprintf("(%s)|(%s)", tastTestsPattern, tastTestsPrivatePattern))
    29  
    30  func fileExists(path string) bool {
    31  	_, err := os.Stat(path)
    32  	return !os.IsNotExist(err)
    33  }
    34  
    35  // isEntryFile checks if path is an entry file.
    36  func isEntryFile(path string) bool {
    37  	var err error
    38  	exPath := path
    39  
    40  	// Unit tests rely on providing fake paths. EvalSymlinks returns an error
    41  	// if the path doesn't exist. To work around this, only evaluate symlinks
    42  	// if the path exists
    43  	if fileExists(exPath) {
    44  		exPath, err = filepath.EvalSymlinks(exPath)
    45  		if err != nil {
    46  			return false
    47  		}
    48  	}
    49  
    50  	exPath, err = filepath.Abs(exPath)
    51  	if err != nil {
    52  		return false
    53  	}
    54  
    55  	return entryPathRegexp.MatchString(exPath) &&
    56  		!isUnitTestFile(exPath) &&
    57  		filepath.Base(exPath) != "doc.go" // exclude package documentation
    58  }
    59  
    60  // isUnitTestFile returns true if the supplied path corresponds to a unit test file.
    61  func isUnitTestFile(path string) bool {
    62  	return strings.HasSuffix(path, "_test.go")
    63  }
    64  
    65  // funcVisitor is an implementation of ast.Visitor to scan all nodes.
    66  type funcVisitor func(node ast.Node)
    67  
    68  func (v funcVisitor) Visit(node ast.Node) ast.Visitor {
    69  	if node == nil {
    70  		return nil
    71  	}
    72  	v(node)
    73  	return v
    74  }
    75  
    76  // toQualifiedName stringifies the given node, which is either
    77  // - an ast.Ident node
    78  // - an ast.SelectorExpr node whose .X node is convertible by toQualifiedName.
    79  // If failed, returns an empty string.
    80  func toQualifiedName(node ast.Node) string {
    81  	var comp []string
    82  	for {
    83  		s, ok := node.(*ast.SelectorExpr)
    84  		if !ok {
    85  			break
    86  		}
    87  		comp = append(comp, s.Sel.Name)
    88  		node = s.X
    89  	}
    90  
    91  	id, ok := node.(*ast.Ident)
    92  	if !ok {
    93  		return ""
    94  	}
    95  	comp = append(comp, id.Name)
    96  
    97  	// Reverse the comp, then join with '.'.
    98  	for i, j := 0, len(comp)-1; i < j; i, j = i+1, j-1 {
    99  		comp[i], comp[j] = comp[j], comp[i]
   100  	}
   101  	return strings.Join(comp, ".")
   102  }
   103  
   104  // stringLitType represent raw string literal or interpreted string literal
   105  // as enumerated value.
   106  type stringLitType int
   107  
   108  const (
   109  	rawStringLit stringLitType = iota
   110  	interpretedStringLit
   111  )
   112  
   113  // stringLitTypeOf returns the string literal type of s. If s does not belong
   114  // to either raw string literal or interpreted string literal, returns false for ok.
   115  func stringLitTypeOf(s string) (strtype stringLitType, ok bool) {
   116  	if s == "" {
   117  		return 0, false
   118  	}
   119  	quote := s[0]
   120  	if quote != s[len(s)-1] {
   121  		return 0, false
   122  	}
   123  	if quote == '`' {
   124  		return rawStringLit, true
   125  	} else if quote == '"' {
   126  		return interpretedStringLit, true
   127  	}
   128  	return 0, false
   129  }
   130  
   131  // quoteAs quotes given unquoted string with double quote or back quote,
   132  // based on stringLiteralType value.
   133  // If specified to be backquoted, but it is impossible, this function
   134  // falls back to the quoting by double-quotes.
   135  func quoteAs(s string, t stringLitType) string {
   136  	if t == rawStringLit && strconv.CanBackquote(s) {
   137  		return "`" + s + "`"
   138  	}
   139  	return strconv.Quote(s)
   140  }
   141  
   142  // formatASTNode returns the byte slice of source code from given file nodes.
   143  func formatASTNode(fs *token.FileSet, f *ast.File) ([]byte, error) {
   144  	var buf bytes.Buffer
   145  	if err := format.Node(&buf, fs, f); err != nil {
   146  		return nil, err
   147  	}
   148  	return buf.Bytes(), nil
   149  }
   150  
   151  // policyNames retrieves the policy names from the defs.go definition file. It
   152  // returns a map where they key represent the name of the policy, and the value
   153  // it a bool which is true for those policies that have the Name function
   154  // defined, false otherwise.
   155  func policyNames() map[string]bool {
   156  	// Get the path to the common file.
   157  	_, filename, _, ok := runtime.Caller(0)
   158  	if !ok {
   159  		panic("Could not get caller information")
   160  	}
   161  
   162  	src := strings.Split(filename, "platform")[0]
   163  	// Build defs path.
   164  	defs := path.Join(src, "platform/tast-tests/src/go.chromium.org/tast-tests/cros/common/policy/defs.go")
   165  	if !fileExists(defs) {
   166  		panic(fmt.Sprintf("Path %s does not exist", defs))
   167  	}
   168  
   169  	fs := token.NewFileSet()
   170  	file, err := parser.ParseFile(fs, defs, nil, parser.ParseComments)
   171  	if err != nil {
   172  		panic(err)
   173  	}
   174  
   175  	m := make(map[string]bool)
   176  
   177  	for _, decl := range file.Decls {
   178  		genDecl, ok := decl.(*ast.GenDecl)
   179  		// Check if declaration is a type definition.
   180  		if ok && genDecl.Tok == token.TYPE && len(genDecl.Specs) == 1 {
   181  			typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec)
   182  			if ok {
   183  				m[typeSpec.Name.Name] = false
   184  			}
   185  
   186  			continue
   187  		}
   188  
   189  		funcDecl, ok := decl.(*ast.FuncDecl)
   190  		// Check if the declaration is the Name function definition implemented for
   191  		// each policy.
   192  		if ok && funcDecl.Name.Name == "Name" && len(funcDecl.Body.List) == 1 {
   193  			returnStmt, ok := funcDecl.Body.List[0].(*ast.ReturnStmt)
   194  			if ok && len(returnStmt.Results) == 1 {
   195  				basicLit, ok := returnStmt.Results[0].(*ast.BasicLit)
   196  				if ok && basicLit.Kind == token.STRING {
   197  					policyName := basicLit.Value[1 : len(basicLit.Value)-1]
   198  					_, ok = m[policyName]
   199  					if ok {
   200  						m[policyName] = true
   201  					}
   202  				}
   203  			}
   204  		}
   205  	}
   206  
   207  	return m
   208  }
   209  
   210  // union adds all key-value pairs from maps a and b. If a and b have an equal key
   211  // then the value from a will be kept.
   212  func union[K comparable, V any](a, b map[K]V) map[K]V {
   213  	c := make(map[K]V)
   214  	for k, v := range a {
   215  		c[k] = v
   216  	}
   217  
   218  	for k, v := range b {
   219  		_, present := c[k]
   220  		if !present {
   221  			c[k] = v
   222  		}
   223  	}
   224  
   225  	return c
   226  }