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 }