index.odin (4259B)
1 package change 2 3 // For every new name, the existing declarations that might already mean 4 // the same thing. Finding them is the cheap part; judging the shortlist is 5 // what the duplication job is for. 6 7 import "core:fmt" 8 import "core:slice" 9 import "core:strings" 10 11 import "../txt" 12 13 // shortlist is how many resembling declarations a new name is shown 14 // beside, ranked; the twins come before them and are not counted. 15 shortlist :: 12 16 17 // find_candidates gathers, per new name, the declarations holding the 18 // same literal — the strongest hint a fact has been written twice — and 19 // then the ones whose names share a word. Both are kept on the change, 20 // keyed by the new name. 21 find_candidates :: proc(c: ^Change, allocator := context.allocator) { 22 context.allocator = allocator 23 c.twins = make(map[string][]string) 24 c.candidates = make(map[string][]string) 25 for symbol in c.symbols { 26 twins := make([dynamic]string) 27 for declared in c.index { 28 if declared.file == symbol.file && declared.line == symbol.line { 29 continue 30 } 31 if same(declared.text, symbol.signature) { 32 append( 33 &twins, 34 fmt.aprintf("%s <- same value", describe(declared, context.temp_allocator)), 35 ) 36 } 37 } 38 if len(twins) > 0 { 39 c.twins[symbol.name] = twins[:] 40 } 41 near := resembling(c.index, symbol, shortlist) 42 listed := make([dynamic]string) 43 append(&listed, ..twins[:]) 44 append(&listed, ..near) 45 c.candidates[symbol.name] = listed[:] 46 } 47 } 48 49 // describe is a declaration as the shortlist names it: where it is, and 50 // the line it was declared on. 51 describe :: proc(d: Declared, allocator := context.allocator) -> string { 52 return fmt.aprintf( 53 "%s:%d: %s", 54 d.file, 55 d.line, 56 strings.trim_space(d.text), 57 allocator = allocator, 58 ) 59 } 60 61 // resembling returns the declarations whose names share a word with the 62 // one given, which is the shortlist a reader is asked to judge. Words are 63 // matched whole, so that cache does not pull in every Cached, and the 64 // list is ranked: the more words shared the higher, a declaration of the 65 // same kind above one of another, and the same file above the rest. 66 resembling :: proc( 67 index: []Declared, 68 symbol: Symbol, 69 limit: int, 70 allocator := context.allocator, 71 ) -> []string { 72 wanted := make(map[string]bool, context.temp_allocator) 73 for word in txt.split_words(symbol.name, context.temp_allocator) { 74 if len(word) >= 4 { 75 wanted[txt.depluralise(strings.to_lower(word, context.temp_allocator))] = true 76 } 77 } 78 if len(wanted) == 0 { 79 return nil 80 } 81 Candidate :: struct { 82 line: string, 83 score: int, 84 order: int, 85 } 86 out := make([dynamic]Candidate, context.temp_allocator) 87 seen := make(map[string]bool, context.temp_allocator) 88 for declared, i in index { 89 if declared.file == symbol.file && declared.line == symbol.line { 90 continue 91 } 92 shared := 0 93 for word in txt.split_words(declared.name, context.temp_allocator) { 94 if wanted[txt.depluralise(strings.to_lower(word, context.temp_allocator))] { 95 shared += 1 96 } 97 } 98 if shared == 0 { 99 continue 100 } 101 line := describe(declared, context.temp_allocator) 102 if seen[line] { 103 continue 104 } 105 seen[line] = true 106 score := shared * 4 107 if declared.kind == symbol.kind { 108 score += 2 109 } 110 if declared.file == symbol.file { 111 score += 1 112 } 113 append(&out, Candidate{line, score, i}) 114 } 115 slice.sort_by_cmp(out[:], proc(a, b: Candidate) -> slice.Ordering { 116 if a.score != b.score { 117 return .Less if a.score > b.score else .Greater 118 } 119 return .Less if a.order < b.order else (.Greater if a.order > b.order else .Equal) 120 }) 121 kept := out[:min(len(out), limit)] 122 lines := make([]string, len(kept), allocator) 123 for r, i in kept { 124 lines[i] = strings.clone(r.line, allocator) 125 } 126 return lines 127 } 128 129 // same reports whether two declaration lines hold the same literal. Go 130 // and TypeScript write `name = value`; Odin writes `name :: value`. 131 same :: proc(a, b: string) -> bool { 132 left, ok_a := literal_of(a) 133 right, ok_b := literal_of(b) 134 return ok_a && ok_b && strings.trim_space(left) == strings.trim_space(right) 135 } 136 137 literal_of :: proc(text: string) -> (string, bool) { 138 if i := strings.index_byte(text, '='); i >= 0 { 139 return text[i + 1:], true 140 } 141 if i := strings.index(text, "::"); i >= 0 { 142 return text[i + 2:], true 143 } 144 return "", false 145 }