github.com/blueinnovationsgroup/can-go@v0.0.0-20230518195432-d0567cda0028/cmd/cantool/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  	"text/scanner"
    14  
    15  	"github.com/blueinnovationsgroup/can-go/internal/generate"
    16  	"github.com/blueinnovationsgroup/can-go/pkg/dbc"
    17  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis"
    18  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/definitiontypeorder"
    19  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/intervals"
    20  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/lineendings"
    21  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/messagenames"
    22  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/multiplexedsignals"
    23  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/newsymbols"
    24  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/nodereferences"
    25  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/noreservedsignals"
    26  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/requireddefinitions"
    27  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/signalbounds"
    28  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/signalnames"
    29  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/singletondefinitions"
    30  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/siunits"
    31  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/uniquenodenames"
    32  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/uniquesignalnames"
    33  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/unitsuffixes"
    34  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/valuedescriptions"
    35  	"github.com/blueinnovationsgroup/can-go/pkg/dbc/analysis/passes/version"
    36  	"github.com/fatih/color"
    37  	"gopkg.in/alecthomas/kingpin.v2"
    38  )
    39  
    40  func main() {
    41  	app := kingpin.New("cantool", "CAN tool for Go programmers")
    42  	generateCommand(app)
    43  	lintCommand(app)
    44  	kingpin.MustParse(app.Parse(os.Args[1:]))
    45  }
    46  
    47  type SignalFilters map[string]map[string]*bool
    48  
    49  func generateCommand(app *kingpin.Application) {
    50  	command := app.Command("generate", "generate CAN messages")
    51  	inputFileOrDir := command.
    52  		Arg("input-file-or-dir", "input directory").
    53  		Required().
    54  		ExistingFileOrDir()
    55  	outputDir := command.
    56  		Arg("output-dir", "output directory").
    57  		Required().
    58  		String()
    59  	filter := command.
    60  		Flag("filter", "comma-separated list of filters (<message>:[<signal>])").
    61  		String()
    62  	filterFile := command.
    63  		Flag("filter-file", "path to file containing messages to include, one per line").
    64  		File()
    65  	packageNameOpt := command.
    66  		Flag("package-name", "package name of generated file(s)").
    67  		Short('p').
    68  		String()
    69  
    70  	command.Action(func(c *kingpin.ParseContext) error {
    71  		var signalFilters SignalFilters
    72  		if *filter != "" {
    73  			signalFilters = make(map[string]map[string]*bool)
    74  			for _, e := range strings.Split(*filter, ",") {
    75  				err := parseFilter(e, signalFilters)
    76  				if err != nil {
    77  					return err
    78  				}
    79  			}
    80  		} else if *filterFile != nil {
    81  			signalFilters = make(map[string]map[string]*bool)
    82  			scan := bufio.NewScanner(*filterFile)
    83  			for scan.Scan() {
    84  				err := parseFilter(scan.Text(), signalFilters)
    85  				if err != nil {
    86  					return err
    87  				}
    88  			}
    89  			if scan.Err() != nil {
    90  				fmt.Println("Failed to parse message file", scan.Err())
    91  			}
    92  		}
    93  		if signalFilters != nil {
    94  			fmt.Println("Using filters (case-insensitive):")
    95  			for msg, sigs := range signalFilters {
    96  				fmt.Printf("\t%s:\n", msg)
    97  				for sig := range sigs {
    98  					fmt.Printf("\t\t%s\n", sig)
    99  				}
   100  			}
   101  			defer func() {
   102  				for msg, sigs := range signalFilters {
   103  					for sig, found := range sigs {
   104  						if !*found {
   105  							fmt.Printf("Warning: no signal found matching filter '%s:%s'\n", msg, sig)
   106  						}
   107  					}
   108  				}
   109  			}()
   110  		}
   111  
   112  		return filepath.Walk(*inputFileOrDir, func(p string, i os.FileInfo, err error) error {
   113  			if err != nil {
   114  				return err
   115  			}
   116  			if i.IsDir() || filepath.Ext(p) != ".dbc" {
   117  				return nil
   118  			}
   119  			var relPath string
   120  			if *inputFileOrDir == p {
   121  				relPath = i.Name()
   122  			} else {
   123  				relPath, err = filepath.Rel(*inputFileOrDir, p)
   124  				if err != nil {
   125  					return err
   126  				}
   127  			}
   128  			outputFile := relPath + ".go"
   129  			outputPath := filepath.Join(*outputDir, outputFile)
   130  
   131  			var packageName string
   132  			if *packageNameOpt != "" {
   133  				packageName = *packageNameOpt
   134  			} else {
   135  				packageName = strings.TrimSuffix(path.Base(p), path.Ext(p)) + "can"
   136  				// Remove illegal characters from package name
   137  				packageName = strings.ReplaceAll(packageName, ".", "")
   138  				packageName = strings.ReplaceAll(packageName, "-", "")
   139  				packageName = strings.ReplaceAll(packageName, "_", "")
   140  			}
   141  
   142  			return genGo(p, outputPath, packageName, signalFilters)
   143  		})
   144  	})
   145  }
   146  func boolPtr(b bool) *bool {
   147  	return &b
   148  }
   149  func parseFilter(entry string, filters SignalFilters) error {
   150  	pieces := strings.Split(entry, ":")
   151  	if len(pieces) > 2 {
   152  		return fmt.Errorf("invalid filter entry: '%s', format is <message>[:<signal>]", entry)
   153  	}
   154  	message := strings.ToLower(pieces[0])
   155  	signalSet, ok := filters[message]
   156  	if !ok {
   157  		signalSet = make(map[string]*bool)
   158  	}
   159  	if len(pieces) == 2 {
   160  		signal := strings.ToLower(pieces[1])
   161  		signalSet[signal] = boolPtr(false)
   162  	}
   163  	filters[message] = signalSet
   164  	return nil
   165  }
   166  
   167  func lintCommand(app *kingpin.Application) {
   168  	command := app.Command("lint", "lint DBC files")
   169  	fileOrDir := command.
   170  		Arg("file-or-dir", "DBC file or directory").
   171  		Required().
   172  		ExistingFileOrDir()
   173  	command.Action(func(context *kingpin.ParseContext) error {
   174  		filesToLint, err := resolveFileOrDirectory(*fileOrDir)
   175  		if err != nil {
   176  			return err
   177  		}
   178  		var hasFailed bool
   179  		for _, lintFile := range filesToLint {
   180  			f, err := os.Open(lintFile)
   181  			if err != nil {
   182  				return err
   183  			}
   184  			source, err := io.ReadAll(f)
   185  			if err != nil {
   186  				return err
   187  			}
   188  			p := dbc.NewParser(f.Name(), source)
   189  			if err := p.Parse(); err != nil {
   190  				printError(source, err.Position(), err.Reason(), "parse")
   191  				continue
   192  			}
   193  			for _, a := range analyzers() {
   194  				pass := &analysis.Pass{
   195  					Analyzer: a,
   196  					File:     p.File(),
   197  				}
   198  				if err := a.Run(pass); err != nil {
   199  					return err
   200  				}
   201  				hasFailed = hasFailed || len(pass.Diagnostics) > 0
   202  				for _, d := range pass.Diagnostics {
   203  					printError(source, d.Pos, d.Message, a.Name)
   204  				}
   205  			}
   206  		}
   207  		if hasFailed {
   208  			return errors.New("one or more lint errors")
   209  		}
   210  		return nil
   211  	})
   212  }
   213  
   214  func analyzers() []*analysis.Analyzer {
   215  	return []*analysis.Analyzer{
   216  		// TODO: Re-evaluate if we want boolprefix.Analyzer(), since it creates a lot of churn in vendor schemas
   217  		definitiontypeorder.Analyzer(),
   218  		intervals.Analyzer(),
   219  		lineendings.Analyzer(),
   220  		messagenames.Analyzer(),
   221  		multiplexedsignals.Analyzer(),
   222  		newsymbols.Analyzer(),
   223  		nodereferences.Analyzer(),
   224  		noreservedsignals.Analyzer(),
   225  		requireddefinitions.Analyzer(),
   226  		signalbounds.Analyzer(),
   227  		signalnames.Analyzer(),
   228  		singletondefinitions.Analyzer(),
   229  		siunits.Analyzer(),
   230  		uniquenodenames.Analyzer(),
   231  		uniquesignalnames.Analyzer(),
   232  		unitsuffixes.Analyzer(),
   233  		valuedescriptions.Analyzer(),
   234  		version.Analyzer(),
   235  	}
   236  }
   237  
   238  func genGo(inputFile, outputFile, packageName string, filters SignalFilters) error {
   239  	if err := os.MkdirAll(filepath.Dir(outputFile), 0o755); err != nil {
   240  		return err
   241  	}
   242  	input, err := os.ReadFile(inputFile)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	result, err := generate.Compile(inputFile, packageName, input)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	for _, warning := range result.Warnings {
   251  		return warning
   252  	}
   253  
   254  	if filters != nil {
   255  		skips := make(map[string][]string)
   256  		// Filter in-place for only the messages and signals matching the filter
   257  		allMessages := result.Database.Messages
   258  		result.Database.Messages = result.Database.Messages[:0]
   259  		for _, msg := range allMessages {
   260  			if signalSet, msgMatch := filters[strings.ToLower(msg.Name)]; msgMatch {
   261  				allSignals := msg.Signals
   262  				msg.Signals = msg.Signals[:0]
   263  				for _, sig := range allSignals {
   264  					if sigFound, sigMatch := signalSet[strings.ToLower(sig.Name)]; sigMatch {
   265  						*sigFound = true
   266  						msg.Signals = append(msg.Signals, sig)
   267  					} else {
   268  						skips[msg.Name] = append(skips[msg.Name], sig.Name)
   269  					}
   270  				}
   271  				result.Database.Messages = append(result.Database.Messages, msg)
   272  			} else {
   273  				skips[msg.Name] = make([]string, 0)
   274  			}
   275  		}
   276  		if len(skips) > 0 {
   277  			fmt.Printf("The following messages/signals in %s were ignored due to filtering:\n", inputFile)
   278  			sortedMsgs := make([]string, 0, len(skips))
   279  			for msg := range skips {
   280  				sortedMsgs = append(sortedMsgs, msg)
   281  			}
   282  			sort.Strings(sortedMsgs)
   283  			for _, msg := range sortedMsgs {
   284  				sigs := skips[msg]
   285  				if len(sigs) > 0 {
   286  					sort.Strings(sigs)
   287  					for _, sig := range sigs {
   288  						fmt.Printf("\t%s:%s\n", msg, sig)
   289  					}
   290  				} else {
   291  					fmt.Printf("\t%s:*\n", msg)
   292  				}
   293  			}
   294  		}
   295  	}
   296  	output, err := generate.Database(result.Database)
   297  	if err != nil {
   298  		return err
   299  	}
   300  	if err := os.WriteFile(outputFile, output, 0o600); err != nil {
   301  		return err
   302  	}
   303  	fmt.Println("wrote:", outputFile)
   304  	return nil
   305  }
   306  
   307  func resolveFileOrDirectory(fileOrDirectory string) ([]string, error) {
   308  	fileInfo, err := os.Stat(fileOrDirectory)
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	if !fileInfo.IsDir() {
   313  		return []string{fileOrDirectory}, nil
   314  	}
   315  	var files []string
   316  	if err := filepath.Walk(fileOrDirectory, func(path string, info os.FileInfo, err error) error {
   317  		if !info.IsDir() && filepath.Ext(path) == ".dbc" {
   318  			files = append(files, path)
   319  		}
   320  		return nil
   321  	}); err != nil {
   322  		return nil, err
   323  	}
   324  	return files, nil
   325  }
   326  
   327  func printError(source []byte, pos scanner.Position, msg, name string) {
   328  	fmt.Printf("\n%s: %s (%s)\n", pos, color.RedString("%s", msg), name)
   329  	fmt.Printf("%s\n", getSourceLine(source, pos))
   330  	fmt.Printf("%s\n", caretAtPosition(pos))
   331  }
   332  
   333  func getSourceLine(source []byte, pos scanner.Position) []byte {
   334  	lineStart := pos.Offset
   335  	for lineStart > 0 && source[lineStart-1] != '\n' {
   336  		lineStart--
   337  	}
   338  	lineEnd := pos.Offset
   339  	for lineEnd < len(source) && source[lineEnd] != '\n' {
   340  		lineEnd++
   341  	}
   342  	return source[lineStart:lineEnd]
   343  }
   344  
   345  func caretAtPosition(pos scanner.Position) string {
   346  	return strings.Repeat(" ", pos.Column-1) + color.YellowString("^")
   347  }