github.com/prysmaticlabs/prysm@v1.4.4/tools/specs-checker/check.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/parser"
     7  	"go/token"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strings"
    13  
    14  	"github.com/urfave/cli/v2"
    15  )
    16  
    17  // Regex to find Python's "def".
    18  var reg1 = regexp.MustCompile(`def\s(.*)\(.*`)
    19  
    20  // checkNumRows defines whether tool should check that the spec comment is the last comment of the block, so not only
    21  // it matches the reference snippet, but it also has the same number of rows.
    22  const checkNumRows = false
    23  
    24  func check(cliCtx *cli.Context) error {
    25  	// Obtain reference snippets.
    26  	defs, err := parseSpecs()
    27  	if err != nil {
    28  		return err
    29  	}
    30  
    31  	// Walk the path, and process all contained Golang files.
    32  	fileWalker := func(path string, info os.FileInfo, err error) error {
    33  		if info == nil {
    34  			return fmt.Errorf("invalid input dir %q", path)
    35  		}
    36  		if !strings.HasSuffix(info.Name(), ".go") {
    37  			return nil
    38  		}
    39  		return inspectFile(path, defs)
    40  	}
    41  	return filepath.Walk(cliCtx.String(dirFlag.Name), fileWalker)
    42  }
    43  
    44  func inspectFile(path string, defs map[string][]string) error {
    45  	// Parse source files, and check the pseudo code.
    46  	fset := token.NewFileSet()
    47  	file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
    48  	if err != nil {
    49  		return err
    50  	}
    51  
    52  	ast.Inspect(file, func(node ast.Node) bool {
    53  		stmt, ok := node.(*ast.CommentGroup)
    54  		if !ok {
    55  			return true
    56  		}
    57  		// Ignore comment groups that do not have python pseudo-code.
    58  		chunk := stmt.Text()
    59  		if !reg1.MatchString(chunk) {
    60  			return true
    61  		}
    62  
    63  		// Trim the chunk, so that it starts from Python's "def".
    64  		loc := reg1.FindStringIndex(chunk)
    65  		chunk = chunk[loc[0]:]
    66  
    67  		// Find out Python function name.
    68  		defName, defBody := parseDefChunk(chunk)
    69  		if defName == "" {
    70  			fmt.Printf("%s: cannot parse comment pseudo code\n", fset.Position(node.Pos()))
    71  			return false
    72  		}
    73  
    74  		// Calculate differences with reference implementation.
    75  		refDefs, ok := defs[defName]
    76  		if !ok {
    77  			fmt.Printf("%s: %q is not found in spec docs\n", fset.Position(node.Pos()), defName)
    78  			return false
    79  		}
    80  		if !matchesRefImplementation(defName, refDefs, defBody, fset.Position(node.Pos())) {
    81  			fmt.Printf("%s: %q code does not match reference implementation in specs\n", fset.Position(node.Pos()), defName)
    82  			return false
    83  		}
    84  
    85  		return true
    86  	})
    87  
    88  	return nil
    89  }
    90  
    91  // parseSpecs parses input spec docs into map of function name -> array of function bodies
    92  // (single entity may have several definitions).
    93  func parseSpecs() (map[string][]string, error) {
    94  	loadSpecsFile := func(sb *strings.Builder, specFilePath string) error {
    95  		chunk, err := specFS.ReadFile(specFilePath)
    96  		if err != nil {
    97  			return fmt.Errorf("cannot read specs file: %w", err)
    98  		}
    99  		_, err = sb.Write(chunk)
   100  		if err != nil {
   101  			return fmt.Errorf("cannot copy specs file: %w", err)
   102  		}
   103  		return nil
   104  	}
   105  
   106  	// Traverse all spec files, and aggregate them within as single string.
   107  	var sb strings.Builder
   108  	for dirName, fileNames := range specDirs {
   109  		for _, fileName := range fileNames {
   110  			if err := loadSpecsFile(&sb, path.Join("data", dirName, fileName)); err != nil {
   111  				return nil, err
   112  			}
   113  		}
   114  	}
   115  
   116  	// Load file with extra definitions (this allows us to use pseudo-code that is not from specs).
   117  	if err := loadSpecsFile(&sb, path.Join("data", "extra.md")); err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	// Parse docs into function name -> array of function bodies map.
   122  	chunks := strings.Split(strings.ReplaceAll(sb.String(), "```python", ""), "```")
   123  	defs := make(map[string][]string, len(chunks))
   124  	for _, chunk := range chunks {
   125  		defName, defBody := parseDefChunk(chunk)
   126  		if defName == "" {
   127  			continue
   128  		}
   129  		defs[defName] = append(defs[defName], defBody)
   130  	}
   131  	return defs, nil
   132  }
   133  
   134  // parseDefChunk extract function name and function body from a Python's "def" chunk.
   135  func parseDefChunk(chunk string) (string, string) {
   136  	chunk = strings.TrimLeft(chunk, "\n")
   137  	if chunk == "" {
   138  		return "", ""
   139  	}
   140  	chunkLines := strings.Split(chunk, "\n")
   141  	// Ignore all snippets, that do not define functions.
   142  	if chunkLines[0][:4] != "def " {
   143  		return "", ""
   144  	}
   145  	defMatches := reg1.FindStringSubmatch(chunkLines[0])
   146  	if len(defMatches) < 2 {
   147  		return "", ""
   148  	}
   149  	return strings.Trim(defMatches[1], " "), chunk
   150  }
   151  
   152  // matchesRefImplementation compares input string to reference code snippets (there might be multiple implementations).
   153  func matchesRefImplementation(defName string, refDefs []string, input string, pos token.Position) bool {
   154  	for _, refDef := range refDefs {
   155  		refDefLines := strings.Split(strings.TrimRight(refDef, "\n"), "\n")
   156  		inputLines := strings.Split(strings.TrimRight(input, "\n"), "\n")
   157  
   158  		matchesPerfectly := true
   159  		for i := 0; i < len(refDefLines); i++ {
   160  			a, b := strings.Trim(refDefLines[i], " "), strings.Trim(inputLines[i], " ")
   161  			if a != b {
   162  				matchesPerfectly = false
   163  				break
   164  			}
   165  		}
   166  		// Mark potential issues, when there's some more comments in our code (which might be ok, as we are not required
   167  		// to put specs comments as the last one in the doc block).
   168  		if checkNumRows && len(refDefLines) != len(inputLines) {
   169  			fmt.Printf("%s: %q potentially has issues (comment is longer than reference implementation)\n", pos, defName)
   170  		}
   171  		if matchesPerfectly {
   172  			return true
   173  		}
   174  	}
   175  	return false
   176  }