馃彴 Self-documenting Identifier Name Analyser for Go
1package main
2
3import (
4 "errors"
5 "flag"
6 "fmt"
7 "github.com/Fuwn/kivia/internal/analyze"
8 "github.com/Fuwn/kivia/internal/collect"
9 "github.com/Fuwn/kivia/internal/report"
10 "os"
11 "slices"
12 "strings"
13)
14
15type options struct {
16 Path string
17 OmitContext bool
18 MinimumEvaluationLength int
19 Format string
20 FailOnViolation bool
21 Ignore []string
22}
23
24func parseOptions(arguments []string) (options, error) {
25 flagSet := flag.NewFlagSet("kivia", flag.ContinueOnError)
26
27 flagSet.SetOutput(os.Stderr)
28
29 var parsed options
30 var ignoreValues stringSliceFlag
31
32 flagSet.StringVar(&parsed.Path, "path", "./...", "Path to analyze (directory, file, or ./...).")
33 flagSet.BoolVar(&parsed.OmitContext, "omit-context", false, "Hide usage context in output.")
34 flagSet.IntVar(&parsed.MinimumEvaluationLength, "min-eval-length", 1, "Minimum identifier length in runes to evaluate.")
35 flagSet.StringVar(&parsed.Format, "format", "text", "Output format: text or JSON.")
36 flagSet.BoolVar(&parsed.FailOnViolation, "fail-on-violation", false, "Exit with code 1 when violations are found.")
37 flagSet.Var(&ignoreValues, "ignore", "Ignore violations by matcher. Repeat this flag as needed. Prefixes: name=, kind=, file=, reason=, func=.")
38
39 if err := flagSet.Parse(arguments); err != nil {
40 return options{}, err
41 }
42
43 if parsed.MinimumEvaluationLength < 1 {
44 return options{}, errors.New("The --min-eval-length value must be at least 1.")
45 }
46
47 parsed.Ignore = slices.Clone(ignoreValues)
48
49 return parsed, nil
50}
51
52func run(parsed options) error {
53 identifiers, err := collect.FromPath(parsed.Path)
54
55 if err != nil {
56 return err
57 }
58
59 result, err := analyze.Run(identifiers, analyze.Options{
60 MinEvaluationLength: parsed.MinimumEvaluationLength,
61 })
62
63 if err != nil {
64 return err
65 }
66
67 result = applyIgnoreFilters(result, parsed.Ignore)
68
69 if err := report.Render(os.Stdout, result, parsed.Format, !parsed.OmitContext); err != nil {
70 return err
71 }
72
73 if parsed.FailOnViolation && len(result.Violations) > 0 {
74 return exitCodeError(1)
75 }
76
77 return nil
78}
79
80func main() {
81 parsed, err := parseOptions(os.Args[1:])
82
83 if err != nil {
84 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
85 os.Exit(2)
86 }
87
88 if err := run(parsed); err != nil {
89 var codeError exitCodeError
90
91 if errors.As(err, &codeError) {
92 os.Exit(int(codeError))
93 }
94
95 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
96 os.Exit(1)
97 }
98}
99
100type exitCodeError int
101
102func (errorCode exitCodeError) Error() string {
103 return fmt.Sprintf("Process exited with code %d.", int(errorCode))
104}
105
106type stringSliceFlag []string
107
108func (values *stringSliceFlag) String() string {
109 if values == nil {
110 return ""
111 }
112
113 return strings.Join(*values, ",")
114}
115
116func (values *stringSliceFlag) Set(value string) error {
117 trimmed := strings.TrimSpace(value)
118
119 if trimmed == "" {
120 return errors.New("Ignore matcher cannot be empty.")
121 }
122
123 *values = append(*values, trimmed)
124
125 return nil
126}
127
128func applyIgnoreFilters(result analyze.Result, ignoreMatchers []string) analyze.Result {
129 if len(ignoreMatchers) == 0 || len(result.Violations) == 0 {
130 return result
131 }
132
133 filteredViolations := make([]analyze.Violation, 0, len(result.Violations))
134
135 for _, violation := range result.Violations {
136 if shouldIgnoreViolation(violation, ignoreMatchers) {
137 continue
138 }
139
140 filteredViolations = append(filteredViolations, violation)
141 }
142
143 result.Violations = filteredViolations
144
145 return result
146}
147
148func shouldIgnoreViolation(violation analyze.Violation, ignoreMatchers []string) bool {
149 for _, matcher := range ignoreMatchers {
150 if matchesViolation(matcher, violation) {
151 return true
152 }
153 }
154
155 return false
156}
157
158func matchesViolation(matcher string, violation analyze.Violation) bool {
159 normalizedMatcher := strings.ToLower(strings.TrimSpace(matcher))
160
161 if normalizedMatcher == "" {
162 return false
163 }
164
165 identifier := violation.Identifier
166
167 if strings.HasPrefix(normalizedMatcher, "name=") {
168 return strings.Contains(strings.ToLower(identifier.Name), strings.TrimPrefix(normalizedMatcher, "name="))
169 }
170
171 if strings.HasPrefix(normalizedMatcher, "kind=") {
172 return strings.Contains(strings.ToLower(identifier.Kind), strings.TrimPrefix(normalizedMatcher, "kind="))
173 }
174
175 if strings.HasPrefix(normalizedMatcher, "file=") {
176 return strings.Contains(strings.ToLower(identifier.File), strings.TrimPrefix(normalizedMatcher, "file="))
177 }
178
179 if strings.HasPrefix(normalizedMatcher, "reason=") {
180 return strings.Contains(strings.ToLower(violation.Reason), strings.TrimPrefix(normalizedMatcher, "reason="))
181 }
182
183 if strings.HasPrefix(normalizedMatcher, "func=") {
184 return strings.Contains(strings.ToLower(identifier.Context.EnclosingFunction), strings.TrimPrefix(normalizedMatcher, "func="))
185 }
186
187 composite := strings.ToLower(strings.Join([]string{
188 identifier.Name,
189 identifier.Kind,
190 identifier.File,
191 violation.Reason,
192 identifier.Context.EnclosingFunction,
193 }, " "))
194
195 return strings.Contains(composite, normalizedMatcher)
196}