review

review patchsets using your default editor
Log | Files | Refs

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 }