package main import ( "errors" "fmt" "io/ioutil" "os" "path/filepath" "strings" "text/scanner" "github.com/fiskerinc/cloud-services/pkg/can-go/internal/generate" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/definitiontypeorder" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/intervals" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/lineendings" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/messagenames" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/multiplexedsignals" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/newsymbols" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/nodereferences" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/noreservedsignals" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/requireddefinitions" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/signalbounds" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/signalnames" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/singletondefinitions" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/siunits" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/uniquenodenames" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/uniquesignalnames" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/unitsuffixes" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/valuedescriptions" "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/version" "github.com/fatih/color" "gopkg.in/alecthomas/kingpin.v2" ) func main() { app := kingpin.New("cantool", "CAN tool for Go programmers") generateCommand(app) lintCommand(app) kingpin.MustParse(app.Parse(os.Args[1:])) } func generateCommand(app *kingpin.Application) { command := app.Command("generate", "generate CAN messages") inputDir := command. Arg("input-dir", "input directory"). Required(). ExistingDir() outputDir := command. Arg("output-dir", "output directory"). Required(). String() command.Action(func(c *kingpin.ParseContext) error { return filepath.Walk(*inputDir, func(p string, i os.FileInfo, err error) error { if err != nil { return err } if i.IsDir() || filepath.Ext(p) != ".dbc" { return nil } relPath, err := filepath.Rel(*inputDir, p) if err != nil { return err } outputFile := relPath + ".go" outputPath := filepath.Join(*outputDir, outputFile) return genGo(p, outputPath) }) }) } func lintCommand(app *kingpin.Application) { command := app.Command("lint", "lint DBC files") fileOrDir := command. Arg("file-or-dir", "DBC file or directory"). Required(). ExistingFileOrDir() command.Action(func(context *kingpin.ParseContext) error { filesToLint, err := resolveFileOrDirectory(*fileOrDir) if err != nil { return err } var hasFailed bool for _, lintFile := range filesToLint { f, err := os.Open(lintFile) if err != nil { return err } source, err := ioutil.ReadAll(f) if err != nil { return err } p := dbc.NewParser(f.Name(), source) if err := p.Parse(); err != nil { printError(source, err.Position(), err.Reason(), "parse") continue } for _, a := range analyzers() { pass := &analysis.Pass{ Analyzer: a, File: p.File(), } if err := a.Run(pass); err != nil { return err } hasFailed = hasFailed || len(pass.Diagnostics) > 0 for _, d := range pass.Diagnostics { printError(source, d.Pos, d.Message, a.Name) } } } if hasFailed { return errors.New("one or more lint errors") } return nil }) } func analyzers() []*analysis.Analyzer { return []*analysis.Analyzer{ // TODO: Re-evaluate if we want boolprefix.Analyzer(), since it creates a lot of churn in vendor schemas definitiontypeorder.Analyzer(), intervals.Analyzer(), lineendings.Analyzer(), messagenames.Analyzer(), multiplexedsignals.Analyzer(), newsymbols.Analyzer(), nodereferences.Analyzer(), noreservedsignals.Analyzer(), requireddefinitions.Analyzer(), signalbounds.Analyzer(), signalnames.Analyzer(), singletondefinitions.Analyzer(), siunits.Analyzer(), uniquenodenames.Analyzer(), uniquesignalnames.Analyzer(), unitsuffixes.Analyzer(), valuedescriptions.Analyzer(), version.Analyzer(), } } func genGo(inputFile, outputFile string) error { if err := os.MkdirAll(filepath.Dir(outputFile), 0o755); err != nil { return err } input, err := ioutil.ReadFile(inputFile) if err != nil { return err } result, err := generate.Compile(inputFile, input) if err != nil { return err } for _, warning := range result.Warnings { return warning } output, err := generate.Database(result.Hash, result.Database) if err != nil { return err } if err := ioutil.WriteFile(outputFile, output, 0o600); err != nil { return err } fmt.Println("hash:", result.Hash) fmt.Println("version:", result.Database.Version) fmt.Println("wrote:", outputFile) return nil } func resolveFileOrDirectory(fileOrDirectory string) ([]string, error) { fileInfo, err := os.Stat(fileOrDirectory) if err != nil { return nil, err } if !fileInfo.IsDir() { return []string{fileOrDirectory}, nil } var files []string if err := filepath.Walk(fileOrDirectory, func(path string, info os.FileInfo, err error) error { if !info.IsDir() && filepath.Ext(path) == ".dbc" { files = append(files, path) } return nil }); err != nil { return nil, err } return files, nil } func printError(source []byte, pos scanner.Position, msg, name string) { fmt.Printf("\n%s: %s (%s)\n", pos, color.RedString("%s", msg), name) fmt.Printf("%s\n", getSourceLine(source, pos)) fmt.Printf("%s\n", caretAtPosition(pos)) } func getSourceLine(source []byte, pos scanner.Position) []byte { lineStart := pos.Offset for lineStart > 0 && source[lineStart-1] != '\n' { lineStart-- } lineEnd := pos.Offset for lineEnd < len(source) && source[lineEnd] != '\n' { lineEnd++ } return source[lineStart:lineEnd] } func caretAtPosition(pos scanner.Position) string { return strings.Repeat(" ", pos.Column-1) + color.YellowString("^") }