review

review patchsets using your default editor
Log | Files | Refs

rules.odin (6263B)


      1 package check
      2 
      3 // The rules are what an agent is handed. A finding cites one by id, and
      4 // the id has to lead somewhere: the description of the deterministic check
      5 // that measured it. Printing them from the binary is what makes the rules
      6 // travel with it.
      7 
      8 import "core:fmt"
      9 import "core:strings"
     10 
     11 // Rule describes one deterministic check, for the reader who met its id
     12 // in a finding.
     13 Rule :: struct {
     14 	id:          string,
     15 	description: string,
     16 }
     17 
     18 // rules are the deterministic checks, in the order the readme lists
     19 // them. Each id here is one a finding can carry with job "static".
     20 rules := []Rule {
     21 	{
     22 		"go-build",
     23 		"the Go compiler cannot build a package the change touched; must-fix wherever the error lands",
     24 	},
     25 	{
     26 		"go-vet/<analyzer>",
     27 		"go vet's finding, on a line the change adds — with review-vet's extra analysers where it is installed; nilness and vet's own set are must-fix, shadow and unusedwrite consider, modernize note",
     28 	},
     29 	{
     30 		"staticcheck/<code>",
     31 		"the Go analyser's finding, on a line the change adds; SA is must-fix, S and U consider, the rest note",
     32 	},
     33 	{
     34 		"odin-check",
     35 		"odin check -vet -strict-style cannot check a package the change touched: a type error is must-fix wherever it lands, a vet failure consider, a style failure note",
     36 	},
     37 	{
     38 		"tsc/<code>",
     39 		"the TypeScript compiler's error under the project's own tsconfig; must-fix wherever it lands; working tree only",
     40 	},
     41 	{
     42 		"ruff/<code>",
     43 		"ruff's finding on a line the change adds; pyflakes codes consider, style codes note",
     44 	},
     45 	{
     46 		"mypy/<code>",
     47 		"mypy's type error on a line the change adds, missing imports ignored; working tree only",
     48 	},
     49 	{
     50 		"cargo/<code>",
     51 		"cargo check's — or clippy's — diagnostic: an error is must-fix wherever it lands, a warning consider on a line the change adds",
     52 	},
     53 	{
     54 		"semgrep/<rule>",
     55 		"a semgrep match on a line the change adds, under the repository's own .semgrep.yml or the registry's p/default pack; the rule's severity is kept: ERROR must-fix, WARNING consider, INFO note",
     56 	},
     57 	{
     58 		"message-low-entropy",
     59 		"the commit message's Shannon entropy is under 3.2 bits per byte: a phrase repeated rather than a description",
     60 	},
     61 	{
     62 		"message-boilerplate",
     63 		"the commit message keeps under 20% of its length after zlib: a block of text pasted or repeated whole",
     64 	},
     65 	{
     66 		"message-common-words",
     67 		"the commit message is made only of the repository's commonest commit words and names nothing the change touches",
     68 	},
     69 	{
     70 		"message-frustration",
     71 		"the commit message is an exclamation — oops, whoops, damn — where a description should be",
     72 	},
     73 	{
     74 		"message-not-imperative",
     75 		"the subject's first word after its package prefix is past tense, a gerund, or the author",
     76 	},
     77 	{
     78 		"message-no-body",
     79 		"a change over 50 lines carries no body, and the diff does not only move text around",
     80 	},
     81 	{"message-long-body", "the body is over 150 words, listing what the diff already shows"},
     82 	{
     83 		"history-coupled-file",
     84 		"history ties a changed file to a partner the change does not touch: Jaccard at least 0.7 over at least 5 shared commits",
     85 	},
     86 	{
     87 		"message-names-unknown",
     88 		"the commit message names an identifier, path or call that is in neither the diff nor the repository at the end of the change",
     89 	},
     90 	{
     91 		"formatting-mixed-in",
     92 		"half or more of the change's lines change only whitespace, and at least 10 change something else: a reformatting with logic in it",
     93 	},
     94 	{
     95 		"suppression-added",
     96 		"the change adds its own review:ignore dismissal, before the readers have run",
     97 	},
     98 	{"test-deleted", "the change deletes a test function that no added test renames"},
     99 	{"no-stutter", "an exported name's first word is its package: ico.IcoEntry says ico twice"},
    100 	{
    101 		"no-shadow",
    102 		"a new name is a predeclared identifier, a standard library package, or a runtime global",
    103 	},
    104 	{"abbreviation", "a new name carries an invented abbreviation: cfg, mgr, hdlr, svc, btn, cnt"},
    105 	{
    106 		"test-no-assertion",
    107 		"an added or altered test has no call that could fail it; it can only fail by crashing",
    108 	},
    109 	{
    110 		"assertion-always-true",
    111 		"an assertion in an added or altered test holds whatever the code does: a literal true, two literals, or a value against itself",
    112 	},
    113 	{
    114 		"duplicate-body",
    115 		"a new function's body already exists, token for token (must-fix) or in shape with every name changed (consider)",
    116 	},
    117 	{
    118 		"function-too-long",
    119 		"a new function is over 150 lines; 95% of measured functions fit in 109",
    120 	},
    121 	{
    122 		"nesting-too-deep",
    123 		"a new function nests blocks more than 5 deep; 99% of measured functions stay within 6",
    124 	},
    125 	{
    126 		"comment-restates-code",
    127 		"every content word of an added comment is in the line of code below it",
    128 	},
    129 	{
    130 		"todo-without-reference",
    131 		"an added TODO, FIXME, XXX or HACK names no issue, ticket, link or person",
    132 	},
    133 	{
    134 		"commented-out-code",
    135 		"an added comment run holds code: two statement-shaped lines, or one beyond doubt",
    136 	},
    137 	{
    138 		"debug-leftover",
    139 		"an added line is a debugger statement, a breakpoint, dbg!, spew.Dump, console.log, or a DEBUG print",
    140 	},
    141 	{
    142 		"error-swallowed",
    143 		"an added line drops an error: _ = err, an empty catch, an except that passes",
    144 	},
    145 	{
    146 		"new-symbol-unreferenced",
    147 		"nothing in the repository refers to a new declaration but its own line",
    148 	},
    149 	{
    150 		"code-without-tests",
    151 		"the change adds 50 or more lines of code, touches no test, and the repository keeps tests",
    152 	},
    153 }
    154 
    155 // catalogue is every rule, as review rules prints them.
    156 catalogue :: proc(allocator := context.allocator) -> string {
    157 	b := strings.builder_make(allocator)
    158 	strings.write_string(&b, "# Deterministic checks\n\n")
    159 	for r in rules {
    160 		fmt.sbprintf(&b, "- `%s` — %s\n", r.id, r.description)
    161 	}
    162 	return strings.to_string(b)
    163 }
    164 
    165 // describe is one rule's description, by its id or by the family a
    166 // tool's rules share: go-vet/anything is go-vet/<analyzer>.
    167 describe :: proc(id: string) -> (description: string, ok: bool) {
    168 	family := id
    169 	if i := strings.index_byte(id, '/'); i >= 0 {
    170 		family = id[:i]
    171 	}
    172 	for r in rules {
    173 		if r.id == id ||
    174 		   (strings.contains(id, "/") &&
    175 				   strings.has_prefix(
    176 					   r.id,
    177 					   strings.concatenate({family, "/"}, context.temp_allocator),
    178 				   )) {
    179 			return r.description, true
    180 		}
    181 	}
    182 	return "", false
    183 }