github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/cmd/tast-lint/internal/check/check_lacros.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  	"go/ast"
     9  	"go/token"
    10  
    11  	"golang.org/x/tools/go/ast/astutil"
    12  
    13  	"go.chromium.org/tast/core/cmd/tast-lint/internal/git"
    14  )
    15  
    16  const (
    17  	noLacrosStatusMsg           = `Test LacrosStatus field should exist`
    18  	nonLiteralLacrosMetadataMsg = `Test LacrosStatus should be a single LacrosStatus value, e.g. testing.LacrosVariantNeeded`
    19  	noLacrosVariantUnknownMsg   = `Test LacrosStatus should not be LacrosVariantUnknown. Please work out if your test needs lacros variants. To do this, see go/lacros-tast-porting or contact edcourtney@, hidehiko@, or lacros-tast@`
    20  	addedUnknownMetadataMsg     = `tast-lint has added LacrosVariantUnknown as a placeholder but please work out if your test needs lacros variants. To do this, see go/lacros-tast-porting or contact edcourtney@, hidehiko@, or lacros-tast@`
    21  	noLacrosSoftwareDepsMsg     = `Test SoftwareDeps dep:lacros should be added along with dep:lacros_{un}stable`
    22  )
    23  
    24  func hasSoftwareDeps(expr ast.Expr, dep string) bool {
    25  	switch v := expr.(type) {
    26  	case *ast.BasicLit:
    27  		return v.Value == "\""+dep+"\""
    28  	case *ast.Ident:
    29  	case *ast.SelectorExpr:
    30  		// We can't really compute the value of constants so just assume it
    31  		// was the dep value we are looking for.
    32  		return true
    33  	case *ast.CompositeLit:
    34  		for _, arg := range v.Elts {
    35  			if hasSoftwareDeps(arg, dep) {
    36  				return true
    37  			}
    38  		}
    39  		return false
    40  	case *ast.CallExpr:
    41  		fun, ok := v.Fun.(*ast.Ident)
    42  		if !ok || fun.Name != "append" {
    43  			return false
    44  		}
    45  		for i, arg := range v.Args {
    46  			isVarList := i == 0 || (i == len(v.Args)-1 && v.Ellipsis != token.NoPos)
    47  			if isVarList {
    48  				if hasSoftwareDeps(arg, dep) {
    49  					return true
    50  				}
    51  			}
    52  			if !isVarList && !hasSoftwareDeps(arg, dep) {
    53  				return false
    54  			}
    55  		}
    56  		return false
    57  	}
    58  
    59  	// If we can't show there is no software dep, assume it exists.
    60  	return true
    61  }
    62  
    63  func checkAllSoftwareDeps(fields entityFields, dep string) bool {
    64  	// Check for chrome SoftwareDeps.
    65  	s, ok := fields["SoftwareDeps"]
    66  	if ok && hasSoftwareDeps(s.Value, dep) {
    67  		return true
    68  	}
    69  
    70  	// Check ExtraSoftwareDeps:
    71  	s, ok = fields["Params"]
    72  	if !ok {
    73  		return false // Was not in SoftwareDeps and no Params.
    74  	}
    75  
    76  	v, ok := s.Value.(*ast.CompositeLit)
    77  	if !ok {
    78  		return true // Expect Params to be a CompositeLit.
    79  	}
    80  
    81  	for _, arg := range v.Elts {
    82  		// Extract each Param
    83  		v, ok := arg.(*ast.CompositeLit)
    84  		if !ok {
    85  			return false
    86  		}
    87  
    88  		for _, paramField := range v.Elts {
    89  			kv, ok := paramField.(*ast.KeyValueExpr)
    90  			if !ok {
    91  				return false
    92  			}
    93  			id, ok := kv.Key.(*ast.Ident)
    94  			if !ok {
    95  				return false
    96  			}
    97  
    98  			if id.Name == "ExtraSoftwareDeps" && hasSoftwareDeps(kv.Value, dep) {
    99  				return true
   100  			}
   101  		}
   102  	}
   103  
   104  	return false
   105  }
   106  
   107  func maybeRewrite(fs *token.FileSet, fields entityFields, call *ast.CallExpr, fix bool, issues []*Issue) []*Issue {
   108  	if fix && len(issues) > 0 {
   109  		f := &ast.KeyValueExpr{
   110  			Key: &ast.Ident{
   111  				Name: "LacrosStatus",
   112  			},
   113  			Value: &ast.SelectorExpr{
   114  				X: &ast.Ident{
   115  					Name: "testing",
   116  				},
   117  				Sel: &ast.Ident{
   118  					Name: "LacrosVariantUnknown",
   119  				},
   120  			},
   121  		}
   122  		if kv, ok := fields["LacrosStatus"]; ok {
   123  			// Try rewriting the field if it already exists.
   124  			astutil.Apply(kv, func(c *astutil.Cursor) bool {
   125  				if p, ok := c.Parent().(*ast.KeyValueExpr); ok && c.Node() == p.Value {
   126  					c.Replace(f.Value)
   127  				}
   128  				return true
   129  			}, nil)
   130  		} else {
   131  			// Otherwise add it after Func which should exist.
   132  			// TODO: This won't add newlines, and there doesn't appear to be any
   133  			// way to do this using astutil.
   134  			astutil.Apply(call, func(c *astutil.Cursor) bool {
   135  				_, parentIsComposite := c.Parent().(*ast.CompositeLit)
   136  				kv, currentIsKeyValue := c.Node().(*ast.KeyValueExpr)
   137  				if parentIsComposite && currentIsKeyValue {
   138  					if id, ok := kv.Key.(*ast.Ident); ok && id.Name == "Func" {
   139  						c.InsertAfter(f)
   140  						return false // Only add the field once.
   141  					}
   142  				}
   143  				return !currentIsKeyValue // Don't recurse into keyvalues, just look at top level testing.Test fields.
   144  			}, nil)
   145  		}
   146  
   147  		return []*Issue{{
   148  			Pos:  fs.Position(call.Args[0].Pos()),
   149  			Msg:  addedUnknownMetadataMsg,
   150  			Link: testRegistrationURL,
   151  		}}
   152  	}
   153  
   154  	return issues
   155  }
   156  
   157  // verifyLacrosStatus verifies that the LacrosStatus field is set, and that
   158  // new instances of LacrosVariantUnknown are not added.
   159  func verifyLacrosStatus(fs *token.FileSet, fields entityFields, path git.CommitFile, call *ast.CallExpr, fix bool) []*Issue {
   160  	// Check for chrome SoftwareDeps.
   161  	if !checkAllSoftwareDeps(fields, "chrome") {
   162  		return nil
   163  	}
   164  
   165  	// For example, extract 'LacrosStatus: testing.LacrosVariantNeeded'.
   166  	kv, ok := fields["LacrosStatus"]
   167  	if !ok {
   168  		return maybeRewrite(fs, fields, call, fix, []*Issue{{
   169  			Pos:     fs.Position(call.Args[0].Pos()),
   170  			Msg:     noLacrosStatusMsg,
   171  			Link:    testRegistrationURL,
   172  			Fixable: true, // We can only add the field, not decide what the value should be.
   173  		}})
   174  	}
   175  
   176  	// For example, extract 'testing.LacrosVariantNeeded'.
   177  	sel, ok := kv.Value.(*ast.SelectorExpr)
   178  	if !ok {
   179  		return maybeRewrite(fs, fields, call, fix, []*Issue{{
   180  			Pos:  fs.Position(kv.Value.Pos()),
   181  			Msg:  nonLiteralLacrosMetadataMsg,
   182  			Link: testRegistrationURL,
   183  		}})
   184  	}
   185  
   186  	// For example, extract 'LacrosVariantUnknown' from 'testing.LacrosVariantNeeded'.
   187  	// Only disallow LacrosVariantUnknown for new tests, to avoid annoying people
   188  	// trying to make incidental changes to existing tests.
   189  	if sel.Sel.Name == "LacrosVariantUnknown" && path.Status == git.Added {
   190  		return maybeRewrite(fs, fields, call, fix, []*Issue{{
   191  			Pos:  fs.Position(kv.Value.Pos()),
   192  			Msg:  noLacrosVariantUnknownMsg,
   193  			Link: testRegistrationURL,
   194  		}})
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  // verifyLacrosSoftwareDeps verifies that the SoftwareDeps 'lacros_{un}stable' should always be
   201  // defined along with 'lacros'.
   202  // Make sure that 'lacros' is a superset of 'lacros_{un}stable' that represent the breakdown
   203  // {un}stable dependencies.
   204  // It makes it easy to share Lacros tests with the 'dep:lacros' between Chromium and ChromiumOS.
   205  func verifyLacrosSoftwareDeps(fs *token.FileSet, fields entityFields, call *ast.CallExpr) []*Issue {
   206  	// Raise an issue only when 'lacros_{un}stable' is set without 'lacros'.
   207  	if (checkAllSoftwareDeps(fields, "lacros_stable") || checkAllSoftwareDeps(fields, "lacros_unstable")) &&
   208  		!checkAllSoftwareDeps(fields, "lacros") {
   209  
   210  		pos := call.Args[0].Pos()
   211  		if s, ok := fields["SoftwareDeps"]; ok {
   212  			pos = s.Pos()
   213  		}
   214  
   215  		return []*Issue{{
   216  			Pos: fs.Position(pos),
   217  			Msg: noLacrosSoftwareDepsMsg,
   218  		}}
   219  	}
   220  
   221  	return nil
   222  }