review

review patchsets using your default editor
Log | Files | Refs

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 }