馃彴 Self-documenting Identifier Name Analyser for Go
at main 196 lines 5.0 kB view raw
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}