Last active
January 19, 2020 10:46
Star
You must be signed in to star a gist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Command cuetype like the front-end of a CUE compiler, parses and | |
type-checks a single CUE package. Errors are reported if the analysis | |
fails; otherwise cuetype is quiet (unless -v is set). | |
Without a list of paths, cuetype reads from standard input, which | |
must provide a single CUE source file defining a complete package. | |
With a single directory argument, cuetype checks the CUE files in | |
that directory, comprising a single package. Use -t to include the | |
Otherwise, each path must be the filename of a CUE file belonging | |
to the same package. | |
Imports are processed by importing directly from the source of | |
imported packages (default), or by importing from compiled and | |
installed packages. | |
Usage: | |
cuetype [flags] [path...] | |
The flags are: | |
-t | |
include local test files in a directory (ignored if -x is provided) | |
-x | |
consider only external test files in a directory | |
-e | |
report all errors (not just the first 10) | |
-v | |
verbose mode | |
Flags controlling additional output: | |
-ast | |
print AST (forces -seq) | |
-trace | |
print parse trace (forces -seq) | |
-comments | |
parse comments (ignored unless -ast or -trace is provided) | |
-panic | |
panic on first error | |
Examples: | |
To check the files a.cue, b.cue, and c.cue: | |
cuetype a.cue b.cue c.cue | |
To check an entire package including (in-package) tests in the directory dir and print the processed files: | |
cuetype -t -v dir | |
To verify the output of a pipe: | |
echo "package foo" | cuetype | |
*/ | |
package main | |
import ( | |
"flag" | |
"fmt" | |
goast "go/ast" | |
"io" | |
"io/ioutil" | |
"os" | |
"path/filepath" | |
"sync" | |
gotime "time" | |
"cuelang.org/go/cue" | |
"cuelang.org/go/cue/ast" | |
"cuelang.org/go/cue/build" | |
"cuelang.org/go/cue/errors" | |
"cuelang.org/go/cue/load" | |
"cuelang.org/go/cue/parser" | |
) | |
var ( | |
// main operation modes | |
testFiles = flag.Bool("t", false, "include in-package test files in a directory") | |
xtestFiles = flag.Bool("x", false, "consider only external test files in a directory") | |
allErrors = flag.Bool("e", false, "report all errors, not just the first 10") | |
verbose = flag.Bool("v", false, "verbose mode") | |
// additional output control | |
printAST = flag.Bool("ast", false, "print AST (forces -seq)") | |
printTrace = flag.Bool("trace", false, "print parse trace (forces -seq)") | |
parseComments = flag.Bool("comments", false, "parse comments (ignored unless -ast or -trace is provided)") | |
panicOnError = flag.Bool("panic", false, "panic on first error") | |
) | |
var ( | |
// fset *token.File | |
errorCount = 0 | |
sequential = false | |
parserOptions = []parser.Option{parser.FromVersion(parser.Latest)} | |
) | |
func initParserMode() { | |
if *allErrors { | |
parserOptions = append(parserOptions, parser.AllErrors) | |
} | |
if *printAST { | |
sequential = true | |
} | |
if *printTrace { | |
parserOptions = append(parserOptions, parser.Trace) | |
sequential = true | |
} | |
if *parseComments && (*printAST || *printTrace) { | |
parserOptions = append(parserOptions, parser.ParseComments) | |
} | |
} | |
const usageString = `usage: cuetype [flags] [path ...] | |
The cuetype command, like the front-end of a CUE compiler, parses and | |
type-checks a single CUE package. Errors are reported if the analysis | |
fails; otherwise cuetype is quiet (unless -v is set). | |
Without a list of paths, cuetype reads from standard input, which | |
must provide a single CUE source file defining a complete package. | |
With a single directory argument, cuetype checks the CUE files in | |
that directory, comprising a single package. Use -t to include the | |
(in-package) _test.cue files. Use -x to type check only external | |
test files. | |
Otherwise, each path must be the filename of a CUE file belonging | |
to the same package. | |
` | |
func usage() { | |
fmt.Fprintln(os.Stderr, usageString) | |
flag.PrintDefaults() | |
os.Exit(2) | |
} | |
func report(err error) { | |
if *panicOnError { | |
panic(err) | |
} | |
// mimic go/scanner.PrintError | |
printError := func(w io.Writer, err error) { | |
if errs := errors.Errors(err); errs != nil { | |
errlist := make([]errors.Error, len(errs)) | |
for _, e := range errs { | |
posses := errors.Positions(e) | |
for i, pos := range posses { | |
errlist[i] = errors.Newf(pos, "File: %s,Line: %d,Column: %d", | |
posses[i].Filename(), | |
posses[i].Line(), | |
posses[i].Column()) | |
} | |
} | |
fmt.Fprintf(w, "%v\n", errlist) | |
errorCount += len(errs) | |
} | |
} | |
printError(os.Stderr, err) | |
} | |
func getPkgFiles(args []string) ([]*ast.File, error) { | |
if len(args) == 0 { | |
// stdin | |
file, err := parseStdin() | |
if err != nil { | |
return nil, err | |
} | |
return []*ast.File{file}, nil | |
} | |
if len(args) == 1 { | |
// possibly a directory | |
path := args[0] | |
info, err := os.Stat(path) | |
if err != nil { | |
return nil, err | |
} | |
if info.IsDir() { | |
return parseDir(path) | |
} | |
} | |
// list of files | |
return parseFiles("", args) | |
} | |
func parseStdin() (*ast.File, error) { | |
src, err := ioutil.ReadAll(os.Stdin) | |
if err != nil { | |
return nil, err | |
} | |
return parse("<standard input>", src) | |
} | |
func parseDir(dir string) ([]*ast.File, error) { | |
ctxt := build.NewContext( | |
build.ParseFile(func(name string, src interface{}) (*ast.File, error) { | |
return parser.ParseFile(name, src, parserOptions...) | |
}), | |
) | |
cfg := &load.Config{ | |
Module: filepath.Dir(dir), | |
Context: ctxt, | |
Tests: *xtestFiles || *testFiles, | |
Tools: true, | |
DataFiles: true, | |
} | |
rel := dir | |
if filepath.IsAbs(rel) { | |
cwd, _ := os.Getwd() | |
rel, _ = filepath.Rel(cwd, filepath.Clean(rel)) | |
} | |
instances := load.Instances([]string{rel}, cfg) | |
filenames := make([]string, 0, len(instances)) | |
for _, inst := range instances { | |
if *xtestFiles { | |
filenames = append(filenames, inst.TestCUEFiles...) // nothing XTestCUEFiles field | |
continue | |
} | |
filenames = append(filenames, append(inst.CUEFiles, inst.ToolCUEFiles...)...) | |
if *testFiles { | |
filenames = append(filenames, inst.TestCUEFiles...) | |
} | |
} | |
return parseFiles(dir, filenames) | |
} | |
func parseFiles(dir string, filenames []string) ([]*ast.File, error) { | |
files := make([]*ast.File, len(filenames)) | |
errs := make([]error, len(filenames)) | |
var wg sync.WaitGroup | |
for i, filename := range filenames { | |
wg.Add(1) | |
go func(i int, f string) { | |
defer wg.Done() | |
files[i], errs[i] = parse(f, nil) | |
}(i, filepath.Join(dir, filepath.Base(filename))) | |
if sequential { | |
wg.Wait() | |
} | |
} | |
wg.Wait() | |
// If there are errors, return the first one for deterministic results. | |
var first error | |
for _, err := range errs { | |
if err != nil { | |
first = err | |
// If we have an error, some files may be nil. | |
// Remove them. (The go/parser always returns | |
// a possibly partial AST even in the presence | |
// of errors, except if the file doesn't exist | |
// in the first place, in which case it cannot | |
// matter.) | |
i := 0 | |
for _, f := range files { | |
if f != nil { | |
files[i] = f | |
i++ | |
} | |
} | |
files = files[:i] | |
break | |
} | |
} | |
return files, first | |
} | |
func parse(filename string, src interface{}) (*ast.File, error) { | |
if *verbose { | |
fmt.Println(filename) | |
} | |
file, err := parser.ParseFile(filename, src, parserOptions...) | |
if *printAST { | |
goast.Print(nil, file) | |
} | |
return file, err | |
} | |
func checkPkgFiles(files []*ast.File) { | |
type bailout struct{} | |
defer func() { | |
switch p := recover().(type) { | |
case nil, bailout: | |
// normal return or early exit | |
default: | |
// re-panic | |
panic(p) | |
} | |
}() | |
filenames := make([]string, len(files)) | |
for i, file := range files { | |
filenames[i] = file.Filename | |
} | |
instances := cue.Build(load.Instances(filenames, nil)) | |
for _, inst := range instances { | |
if err := inst.Value().Validate(cue.All()); err != nil { | |
if !*allErrors && errorCount >= 10 { | |
panic(bailout{}) | |
} | |
report(err) | |
} | |
} | |
} | |
func printStats(d gotime.Duration) { | |
// fileCount := 0 | |
// lineCount := 0 | |
// fset.Iterate(func(f *token.File) bool { | |
// fileCount++ | |
// lineCount += f.LineCount() | |
// return true | |
// }) | |
// | |
// fmt.Printf( | |
// "%s (%d files, %d lines, %d lines/s)\n", | |
// d, fileCount, lineCount, int64(float64(lineCount)/d.Seconds()), | |
// ) | |
} | |
func main() { | |
flag.Usage = usage | |
flag.Parse() | |
initParserMode() | |
start := gotime.Now() | |
files, err := getPkgFiles(flag.Args()) | |
if err != nil { | |
report(err) | |
// ok to continue (files may be empty, but not nil) | |
} | |
checkPkgFiles(files) | |
if errorCount > 0 { | |
os.Exit(2) | |
} | |
if *verbose { | |
printStats(gotime.Since(start)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment