Skip to content

Instantly share code, notes, and snippets.

@zchee
Last active January 19, 2020 10:46
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save zchee/46194de32a4ea5f332c994dfd810a922 to your computer and use it in GitHub Desktop.
/*
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