馃彴 Self-documenting Identifier Name Analyser for Go
1package analyze
2
3import (
4 "github.com/Fuwn/kivia/internal/collect"
5 "strings"
6 "unicode"
7 "unicode/utf8"
8)
9
10type Options struct {
11 MinEvaluationLength int
12}
13
14type Result struct {
15 Violations []Violation `json:"violations"`
16}
17
18type Violation struct {
19 Identifier collect.Identifier `json:"identifier"`
20 Reason string `json:"reason"`
21}
22
23func Run(identifiers []collect.Identifier, options Options) (Result, error) {
24 minimumEvaluationLength := options.MinEvaluationLength
25
26 if minimumEvaluationLength <= 0 {
27 minimumEvaluationLength = 1
28 }
29
30 resources, err := getResources()
31
32 if err != nil {
33 return Result{}, err
34 }
35
36 violations := make([]Violation, 0)
37
38 for _, identifier := range identifiers {
39 if utf8.RuneCountInString(strings.TrimSpace(identifier.Name)) < minimumEvaluationLength {
40 continue
41 }
42
43 evaluation := evaluateIdentifier(identifier, resources, minimumEvaluationLength)
44
45 if !evaluation.isViolation {
46 continue
47 }
48
49 violation := Violation{
50 Identifier: identifier,
51 Reason: evaluation.reason,
52 }
53 violations = append(violations, violation)
54 }
55
56 return Result{Violations: violations}, nil
57}
58
59type evaluationResult struct {
60 isViolation bool
61 reason string
62}
63
64func evaluateIdentifier(identifier collect.Identifier, resources resources, minimumTokenLength int) evaluationResult {
65 name := strings.TrimSpace(identifier.Name)
66
67 if name == "" {
68 return evaluationResult{}
69 }
70
71 tokens := tokenize(name)
72
73 if len(tokens) == 0 {
74 return evaluationResult{}
75 }
76
77 for _, token := range tokens {
78 if utf8.RuneCountInString(token) < minimumTokenLength {
79 continue
80 }
81
82 if !isAlphabeticToken(token) {
83 continue
84 }
85
86 if resources.dictionary.IsWord(token) {
87 continue
88 }
89
90 if isUpperCaseToken(name, token) {
91 continue
92 }
93
94 if isDisallowedAbbreviation(token, resources) {
95 return evaluationResult{isViolation: true, reason: "Contains abbreviation: " + token + "."}
96 }
97
98 return evaluationResult{isViolation: true, reason: "Term not found in dictionary: " + token + "."}
99 }
100
101 return evaluationResult{}
102}
103
104func isUpperCaseToken(identifierName string, token string) bool {
105 tokenLength := utf8.RuneCountInString(token)
106
107 if tokenLength < 2 || tokenLength > 8 {
108 return false
109 }
110
111 return strings.Contains(identifierName, strings.ToUpper(token))
112}
113
114func tokenize(name string) []string {
115 name = strings.TrimSpace(name)
116
117 if name == "" {
118 return nil
119 }
120
121 parts := strings.FieldsFunc(name, func(r rune) bool {
122 return r == '_' || r == '-' || r == ' '
123 })
124
125 if len(parts) == 0 {
126 return nil
127 }
128
129 result := make([]string, 0, len(parts)*2)
130
131 for _, part := range parts {
132 if part == "" {
133 continue
134 }
135
136 result = append(result, splitCamel(part)...)
137 }
138
139 return result
140}
141
142func splitCamel(input string) []string {
143 if input == "" {
144 return nil
145 }
146
147 runes := []rune(input)
148
149 if len(runes) == 0 {
150 return nil
151 }
152
153 tokens := make([]string, 0, 2)
154 start := 0
155
156 for index := 1; index < len(runes); index++ {
157 current := runes[index]
158 previous := runes[index-1]
159 next := rune(0)
160
161 if index+1 < len(runes) {
162 next = runes[index+1]
163 }
164
165 isBoundary := false
166
167 if unicode.IsLower(previous) && unicode.IsUpper(current) {
168 isBoundary = true
169 }
170
171 if unicode.IsDigit(previous) != unicode.IsDigit(current) {
172 isBoundary = true
173 }
174
175 if unicode.IsUpper(previous) && unicode.IsUpper(current) && next != 0 && unicode.IsLower(next) {
176 isBoundary = true
177 }
178
179 if isBoundary {
180 tokens = append(tokens, strings.ToLower(string(runes[start:index])))
181 start = index
182 }
183 }
184
185 tokens = append(tokens, strings.ToLower(string(runes[start:])))
186
187 return tokens
188}
189
190func isDisallowedAbbreviation(token string, resources resources) bool {
191 _, hasExpansion := resources.dictionary.AbbreviationExpansion(token)
192
193 return hasExpansion
194}
195
196func isAlphabeticToken(token string) bool {
197 if token == "" {
198 return false
199 }
200
201 for _, character := range token {
202 if !unicode.IsLetter(character) {
203 return false
204 }
205 }
206
207 return true
208}