names.odin (5937B)
1 package check 2 3 // Three of the naming rules need no judgement. Whether a name repeats its 4 // package, shadows something the language already names, or carries an 5 // invented abbreviation is a comparison against a list. 6 7 import "base:runtime" 8 import "core:fmt" 9 import "core:strings" 10 11 import "../change" 12 import "../finding" 13 import "../txt" 14 15 @(private = "file") 16 predeclared: map[string]bool 17 @(private = "file") 18 stdlib: map[string]bool 19 @(private = "file") 20 globals: map[string]bool 21 @(private = "file") 22 builtins: map[string]bool 23 @(private = "file") 24 abbreviations: map[string]bool 25 26 @(init) 27 init_name_lists :: proc "contextless" () { 28 context = runtime.default_context() 29 // Go's universe-block identifiers. A package-level name that takes 30 // one compiles, and then the builtin is gone for the whole package. 31 predeclared = set( 32 `append bool byte cap clear close complex complex64 complex128 copy delete error false 33 float32 float64 imag int int8 int16 int32 int64 iota len make max min new nil panic print println 34 real recover rune string true uint uint8 uint16 uint32 uint64 uintptr any comparable`, 35 ) 36 // The standard library packages a Go file is likeliest to import. 37 stdlib = set( 38 `bufio bytes cmp context errors fmt io log maps math os path reflect regexp slices sort 39 strconv strings sync testing time unicode url http json exec filepath rand hash flag template`, 40 ) 41 // The names a browser or Node runtime already binds. 42 globals = set( 43 `Promise Map Set Array Object Error JSON Math Date Symbol String Number Boolean console 44 window document process require module exports fetch event location history navigator`, 45 ) 46 // Python's builtins, the ones a module-level name is likeliest to take 47 // by accident. 48 builtins = set( 49 `abs all any bin bool bytes callable chr dict dir divmod enumerate eval exec filter 50 float format frozenset getattr hasattr hash help hex id input int isinstance issubclass iter len list 51 locals map max min next object oct open ord pow print property range repr reversed round set setattr 52 slice sorted str sum super tuple type vars zip`, 53 ) 54 // The shortenings the discipline rejects. Established ones — id, url, 55 // ctx, msg, err, buf, cmd, tmp — are words in their own right. 56 abbreviations = set( 57 `cfg mgr mgmt hdlr hndlr hndl svc ctrl ctlr btn cnt amt qty calc tbl usr pwd dflt nbr mdl 58 srvr clnt rslt chk upd`, 59 ) 60 } 61 62 // check_names reports the naming faults a comparison can settle: stutter 63 // against the package, shadowing of a predeclared or well-known name, 64 // and abbreviations the discipline rejects. 65 check_names :: proc(s: Scope, out: ^[dynamic]finding.Finding) { 66 for sym in s.c.symbols { 67 if is_test_file(sym.file) { 68 continue 69 } 70 stutter(sym, out) 71 shadow(sym, out) 72 abbreviated(sym, out) 73 } 74 } 75 76 // stutter reports a name whose first word is its package. The package's 77 // name is already said wherever the name is used, so the word is said 78 // twice: ico.IcoEntry. A type named exactly for its package is the 79 // language's own idiom — time.Time — and is spared, as is anything in 80 // package main, which nothing qualifies. 81 stutter :: proc(sym: change.Symbol, out: ^[dynamic]finding.Finding) { 82 if sym.pkg == "" || sym.pkg == "main" || !sym.exported { 83 return 84 } 85 words := txt.split_words(sym.name, context.temp_allocator) 86 if len(words) < 2 { 87 return 88 } 89 pkg, _ := strings.replace_all(sym.pkg, "_", "", context.temp_allocator) 90 if strings.to_lower(words[0], context.temp_allocator) != 91 strings.to_lower(pkg, context.temp_allocator) { 92 return 93 } 94 append( 95 out, 96 static( 97 "no-stutter", 98 .Consider, 99 fmt.aprintf( 100 "%s repeats its package: %s.%s says %s twice", 101 sym.name, 102 sym.pkg, 103 sym.name, 104 words[0], 105 ), 106 fmt.aprintf( 107 "drop the package's word: %s.%s", 108 sym.pkg, 109 strings.join(words[1:], "", context.temp_allocator), 110 ), 111 file = sym.file, 112 line = sym.line, 113 symbol = sym.name, 114 ), 115 ) 116 } 117 118 // shadow reports a name the language or its runtime already means 119 // something by: Go's predeclared identifiers and standard library 120 // packages, the runtime's globals for TypeScript and JavaScript, the 121 // builtins for Python. 122 shadow :: proc(sym: change.Symbol, out: ^[dynamic]finding.Finding) { 123 what, why: string 124 switch { 125 case strings.has_suffix(sym.file, ".go"): 126 switch { 127 case predeclared[sym.name]: 128 what, why = "a predeclared identifier", "the builtin is gone for the whole package" 129 case stdlib[sym.name]: 130 what, why = 131 "a standard library package", 132 "no file in the package can import it beside this name" 133 } 134 case grammar_of(sym.file) != "": 135 if globals[sym.name] { 136 what, why = 137 "a runtime global", "the runtime's is shadowed for every reader of the module" 138 } 139 case strings.has_suffix(sym.file, ".py"): 140 if builtins[sym.name] { 141 what, why = "a builtin", "the builtin is gone for the whole module" 142 } 143 } 144 if what == "" { 145 return 146 } 147 append( 148 out, 149 static( 150 "no-shadow", 151 .Consider, 152 fmt.aprintf("%s is %s, and %s", sym.name, what, why), 153 "name it for what it is here, in a word the language does not already use", 154 file = sym.file, 155 line = sym.line, 156 symbol = sym.name, 157 ), 158 ) 159 } 160 161 // abbreviated reports a name carrying an invented abbreviation: a word 162 // the reader has to expand rather than read. 163 abbreviated :: proc(sym: change.Symbol, out: ^[dynamic]finding.Finding) { 164 hit := make([dynamic]string, context.temp_allocator) 165 for word in txt.split_words(sym.name, context.temp_allocator) { 166 if abbreviations[strings.to_lower(word, context.temp_allocator)] { 167 append(&hit, word) 168 } 169 } 170 if len(hit) == 0 { 171 return 172 } 173 append( 174 out, 175 static( 176 "abbreviation", 177 .Consider, 178 fmt.aprintf( 179 "%s abbreviates %s; an invented abbreviation is a word the reader expands rather than reads", 180 sym.name, 181 strings.join(hit[:], ", ", context.temp_allocator), 182 ), 183 "write the word out", 184 file = sym.file, 185 line = sym.line, 186 symbol = sym.name, 187 ), 188 ) 189 }