github.com/jshiv/can-go@v0.2.1-0.20210224011015-069e90e90bdf/cmd/cantool/main.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"text/scanner"
    11  
    12  	"github.com/fatih/color"
    13  	"go.einride.tech/can/internal/generate"
    14  	"go.einride.tech/can/pkg/dbc"
    15  	"go.einride.tech/can/pkg/dbc/analysis"
    16  	"go.einride.tech/can/pkg/dbc/analysis/passes/definitiontypeorder"
    17  	"go.einride.tech/can/pkg/dbc/analysis/passes/intervals"
    18  	"go.einride.tech/can/pkg/dbc/analysis/passes/lineendings"
    19  	"go.einride.tech/can/pkg/dbc/analysis/passes/messagenames"
    20  	"go.einride.tech/can/pkg/dbc/analysis/passes/multiplexedsignals"
    21  	"go.einride.tech/can/pkg/dbc/analysis/passes/newsymbols"
    22  	"go.einride.tech/can/pkg/dbc/analysis/passes/nodereferences"
    23  	"go.einride.tech/can/pkg/dbc/analysis/passes/noreservedsignals"
    24  	"go.einride.tech/can/pkg/dbc/analysis/passes/requireddefinitions"
    25  	"go.einride.tech/can/pkg/dbc/analysis/passes/signalbounds"
    26  	"go.einride.tech/can/pkg/dbc/analysis/passes/signalnames"
    27  	"go.einride.tech/can/pkg/dbc/analysis/passes/singletondefinitions"
    28  	"go.einride.tech/can/pkg/dbc/analysis/passes/siunits"
    29  	"go.einride.tech/can/pkg/dbc/analysis/passes/uniquenodenames"
    30  	"go.einride.tech/can/pkg/dbc/analysis/passes/uniquesignalnames"
    31  	"go.einride.tech/can/pkg/dbc/analysis/passes/unitsuffixes"
    32  	"go.einride.tech/can/pkg/dbc/analysis/passes/valuedescriptions"
    33  	"go.einride.tech/can/pkg/dbc/analysis/passes/version"
    34  	"gopkg.in/alecthomas/kingpin.v2"
    35  )
    36  
    37  func main() {
    38  	app := kingpin.New("cantool", "CAN tool for Go programmers")
    39  	generateCommand(app)
    40  	lintCommand(app)
    41  	kingpin.MustParse(app.Parse(os.Args[1:]))
    42  }
    43  
    44  func generateCommand(app *kingpin.Application) {
    45  	command := app.Command("generate", "generate CAN messages")
    46  	inputDir := command.
    47  		Arg("input-dir", "input directory").
    48  		Required().
    49  		ExistingDir()
    50  	outputDir := command.
    51  		Arg("output-dir", "output directory").
    52  		Required().
    53  		String()
    54  	command.Action(func(c *kingpin.ParseContext) error {
    55  		return filepath.Walk(*inputDir, func(p string, i os.FileInfo, err error) error {
    56  			if err != nil {
    57  				return err
    58  			}
    59  			if i.IsDir() || filepath.Ext(p) != ".dbc" {
    60  				return nil
    61  			}
    62  			relPath, err := filepath.Rel(*inputDir, p)
    63  			if err != nil {
    64  				return err
    65  			}
    66  			outputFile := relPath + ".go"
    67  			outputPath := filepath.Join(*outputDir, outputFile)
    68  			return genGo(p, outputPath)
    69  		})
    70  	})
    71  }
    72  
    73  func lintCommand(app *kingpin.Application) {
    74  	command := app.Command("lint", "lint DBC files")
    75  	fileOrDir := command.
    76  		Arg("file-or-dir", "DBC file or directory").
    77  		Required().
    78  		ExistingFileOrDir()
    79  	command.Action(func(context *kingpin.ParseContext) error {
    80  		filesToLint, err := resolveFileOrDirectory(*fileOrDir)
    81  		if err != nil {
    82  			return err
    83  		}
    84  		var hasFailed bool
    85  		for _, lintFile := range filesToLint {
    86  			f, err := os.Open(lintFile)
    87  			if err != nil {
    88  				return err
    89  			}
    90  			source, err := ioutil.ReadAll(f)
    91  			if err != nil {
    92  				return err
    93  			}
    94  			p := dbc.NewParser(f.Name(), source)
    95  			if err := p.Parse(); err != nil {
    96  				printError(source, err.Position(), err.Reason(), "parse")
    97  				continue
    98  			}
    99  			for _, a := range analyzers() {
   100  				pass := &analysis.Pass{
   101  					Analyzer: a,
   102  					File:     p.File(),
   103  				}
   104  				if err := a.Run(pass); err != nil {
   105  					return err
   106  				}
   107  				hasFailed = hasFailed || len(pass.Diagnostics) > 0
   108  				for _, d := range pass.Diagnostics {
   109  					printError(source, d.Pos, d.Message, a.Name)
   110  				}
   111  			}
   112  		}
   113  		if hasFailed {
   114  			return errors.New("one or more lint errors")
   115  		}
   116  		return nil
   117  	})
   118  }
   119  
   120  func analyzers() []*analysis.Analyzer {
   121  	return []*analysis.Analyzer{
   122  		// TODO: Re-evaluate if we want boolprefix.Analyzer(), since it creates a lot of churn in vendor schemas
   123  		definitiontypeorder.Analyzer(),
   124  		intervals.Analyzer(),
   125  		lineendings.Analyzer(),
   126  		messagenames.Analyzer(),
   127  		multiplexedsignals.Analyzer(),
   128  		newsymbols.Analyzer(),
   129  		nodereferences.Analyzer(),
   130  		noreservedsignals.Analyzer(),
   131  		requireddefinitions.Analyzer(),
   132  		signalbounds.Analyzer(),
   133  		signalnames.Analyzer(),
   134  		singletondefinitions.Analyzer(),
   135  		siunits.Analyzer(),
   136  		uniquenodenames.Analyzer(),
   137  		uniquesignalnames.Analyzer(),
   138  		unitsuffixes.Analyzer(),
   139  		valuedescriptions.Analyzer(),
   140  		version.Analyzer(),
   141  	}
   142  }
   143  
   144  func genGo(inputFile, outputFile string) error {
   145  	if err := os.MkdirAll(filepath.Dir(outputFile), 0o755); err != nil {
   146  		return err
   147  	}
   148  	input, err := ioutil.ReadFile(inputFile)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	result, err := generate.Compile(inputFile, input)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	for _, warning := range result.Warnings {
   157  		return warning
   158  	}
   159  	output, err := generate.Database(result.Database)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	if err := ioutil.WriteFile(outputFile, output, 0o600); err != nil {
   164  		return err
   165  	}
   166  	fmt.Println("wrote:", outputFile)
   167  	return nil
   168  }
   169  
   170  func resolveFileOrDirectory(fileOrDirectory string) ([]string, error) {
   171  	fileInfo, err := os.Stat(fileOrDirectory)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  	if !fileInfo.IsDir() {
   176  		return []string{fileOrDirectory}, nil
   177  	}
   178  	var files []string
   179  	if err := filepath.Walk(fileOrDirectory, func(path string, info os.FileInfo, err error) error {
   180  		if !info.IsDir() && filepath.Ext(path) == ".dbc" {
   181  			files = append(files, path)
   182  		}
   183  		return nil
   184  	}); err != nil {
   185  		return nil, err
   186  	}
   187  	return files, nil
   188  }
   189  
   190  func printError(source []byte, pos scanner.Position, msg, name string) {
   191  	fmt.Printf("\n%s: %s (%s)\n", pos, color.RedString("%s", msg), name)
   192  	fmt.Printf("%s\n", getSourceLine(source, pos))
   193  	fmt.Printf("%s\n", caretAtPosition(pos))
   194  }
   195  
   196  func getSourceLine(source []byte, pos scanner.Position) []byte {
   197  	lineStart := pos.Offset
   198  	for lineStart > 0 && source[lineStart-1] != '\n' {
   199  		lineStart--
   200  	}
   201  	lineEnd := pos.Offset
   202  	for lineEnd < len(source) && source[lineEnd] != '\n' {
   203  		lineEnd++
   204  	}
   205  	return source[lineStart:lineEnd]
   206  }
   207  
   208  func caretAtPosition(pos scanner.Position) string {
   209  	return strings.Repeat(" ", pos.Column-1) + color.YellowString("^")
   210  }