diff options
Diffstat (limited to 'main.go')
| -rw-r--r-- | main.go | 196 |
1 files changed, 196 insertions, 0 deletions
@@ -0,0 +1,196 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "github.com/Fuwn/kivia/internal/analyze" + "github.com/Fuwn/kivia/internal/collect" + "github.com/Fuwn/kivia/internal/report" + "os" + "slices" + "strings" +) + +type options struct { + Path string + OmitContext bool + MinimumEvaluationLength int + Format string + FailOnViolation bool + Ignore []string +} + +func parseOptions(arguments []string) (options, error) { + flagSet := flag.NewFlagSet("kivia", flag.ContinueOnError) + + flagSet.SetOutput(os.Stderr) + + var parsed options + var ignoreValues stringSliceFlag + + flagSet.StringVar(&parsed.Path, "path", "./...", "Path to analyze (directory, file, or ./...).") + flagSet.BoolVar(&parsed.OmitContext, "omit-context", false, "Hide usage context in output.") + flagSet.IntVar(&parsed.MinimumEvaluationLength, "min-eval-length", 1, "Minimum identifier length in runes to evaluate.") + flagSet.StringVar(&parsed.Format, "format", "text", "Output format: text or JSON.") + flagSet.BoolVar(&parsed.FailOnViolation, "fail-on-violation", false, "Exit with code 1 when violations are found.") + flagSet.Var(&ignoreValues, "ignore", "Ignore violations by matcher. Repeat this flag as needed. Prefixes: name=, kind=, file=, reason=, func=.") + + if err := flagSet.Parse(arguments); err != nil { + return options{}, err + } + + if parsed.MinimumEvaluationLength < 1 { + return options{}, errors.New("The --min-eval-length value must be at least 1.") + } + + parsed.Ignore = slices.Clone(ignoreValues) + + return parsed, nil +} + +func run(parsed options) error { + identifiers, err := collect.FromPath(parsed.Path) + + if err != nil { + return err + } + + result, err := analyze.Run(identifiers, analyze.Options{ + MinEvaluationLength: parsed.MinimumEvaluationLength, + }) + + if err != nil { + return err + } + + result = applyIgnoreFilters(result, parsed.Ignore) + + if err := report.Render(os.Stdout, result, parsed.Format, !parsed.OmitContext); err != nil { + return err + } + + if parsed.FailOnViolation && len(result.Violations) > 0 { + return exitCodeError(1) + } + + return nil +} + +func main() { + parsed, err := parseOptions(os.Args[1:]) + + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(2) + } + + if err := run(parsed); err != nil { + var codeError exitCodeError + + if errors.As(err, &codeError) { + os.Exit(int(codeError)) + } + + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + +type exitCodeError int + +func (errorCode exitCodeError) Error() string { + return fmt.Sprintf("Process exited with code %d.", int(errorCode)) +} + +type stringSliceFlag []string + +func (values *stringSliceFlag) String() string { + if values == nil { + return "" + } + + return strings.Join(*values, ",") +} + +func (values *stringSliceFlag) Set(value string) error { + trimmed := strings.TrimSpace(value) + + if trimmed == "" { + return errors.New("Ignore matcher cannot be empty.") + } + + *values = append(*values, trimmed) + + return nil +} + +func applyIgnoreFilters(result analyze.Result, ignoreMatchers []string) analyze.Result { + if len(ignoreMatchers) == 0 || len(result.Violations) == 0 { + return result + } + + filteredViolations := make([]analyze.Violation, 0, len(result.Violations)) + + for _, violation := range result.Violations { + if shouldIgnoreViolation(violation, ignoreMatchers) { + continue + } + + filteredViolations = append(filteredViolations, violation) + } + + result.Violations = filteredViolations + + return result +} + +func shouldIgnoreViolation(violation analyze.Violation, ignoreMatchers []string) bool { + for _, matcher := range ignoreMatchers { + if matchesViolation(matcher, violation) { + return true + } + } + + return false +} + +func matchesViolation(matcher string, violation analyze.Violation) bool { + normalizedMatcher := strings.ToLower(strings.TrimSpace(matcher)) + + if normalizedMatcher == "" { + return false + } + + identifier := violation.Identifier + + if strings.HasPrefix(normalizedMatcher, "name=") { + return strings.Contains(strings.ToLower(identifier.Name), strings.TrimPrefix(normalizedMatcher, "name=")) + } + + if strings.HasPrefix(normalizedMatcher, "kind=") { + return strings.Contains(strings.ToLower(identifier.Kind), strings.TrimPrefix(normalizedMatcher, "kind=")) + } + + if strings.HasPrefix(normalizedMatcher, "file=") { + return strings.Contains(strings.ToLower(identifier.File), strings.TrimPrefix(normalizedMatcher, "file=")) + } + + if strings.HasPrefix(normalizedMatcher, "reason=") { + return strings.Contains(strings.ToLower(violation.Reason), strings.TrimPrefix(normalizedMatcher, "reason=")) + } + + if strings.HasPrefix(normalizedMatcher, "func=") { + return strings.Contains(strings.ToLower(identifier.Context.EnclosingFunction), strings.TrimPrefix(normalizedMatcher, "func=")) + } + + composite := strings.ToLower(strings.Join([]string{ + identifier.Name, + identifier.Kind, + identifier.File, + violation.Reason, + identifier.Context.EnclosingFunction, + }, " ")) + + return strings.Contains(composite, normalizedMatcher) +} |