aboutsummaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-27 07:13:17 +0000
committerFuwn <[email protected]>2026-02-27 07:13:17 +0000
commit856e2994722e2e7f67b47d55b8e673ddabcebe83 (patch)
tree5a4e108384038eaa072d8e6c5f71ab68901fb431 /main.go
downloadkivia-main.tar.xz
kivia-main.zip
feat: Initial commitHEADmain
Diffstat (limited to 'main.go')
-rw-r--r--main.go196
1 files changed, 196 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..f39546c
--- /dev/null
+++ b/main.go
@@ -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)
+}