| 1 | // markup-spelling: old-spelling begin — the before/after table. |
| 2 | |
| 3 | package markup |
| 4 | |
| 5 | import ( |
| 6 | "regexp" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | // TestTheGrammar pins the translation itself: kind, part, variant, |
| 12 | // part-with-variant, tone, and the things that must NOT move — a |
| 13 | // utility class, an app's own class, and the data-* attributes that |
| 14 | // carry runtime state rather than authored vocabulary. |
| 15 | func TestTheGrammar(t *testing.T) { |
| 16 | for _, c := range []struct{ in, want string }{ |
| 17 | // Kinds, parts, variants. |
| 18 | {`<div class="rst-box">`, `<div rst-box>`}, |
| 19 | {`<div class="rst-callout__body">`, `<div rst-callout-body>`}, |
| 20 | {`<a class="rst-btn rst-btn--primary" href="/x">`, `<a rst-btn="primary" href="/x">`}, |
| 21 | {`<span class="rst-badge rst-badge--positive">`, `<span rst-badge="positive">`}, |
| 22 | {`<div class="rst-dtp__row rst-dtp__row--set">`, `<div rst-dtp-row="set">`}, |
| 23 | {`<div class="rst-lrow rst-lrow--head">`, `<div rst-lrow="head">`}, |
| 24 | // Two kinds on one element become two attributes. |
| 25 | {`<details class="rst-dropdown rst-locale">`, `<details rst-dropdown rst-locale>`}, |
| 26 | // A kind, a variant and a utility: an attribute AND a class. |
| 27 | {`<span class="rst-m-hide rst-cell-mut">`, `<span class="rst-m-hide rst-cell-mut">`}, |
| 28 | {`<a class="rst-nm rst-person rst-person--lg">`, `<a class="rst-nm" rst-person="lg">`}, |
| 29 | // Tone. |
| 30 | {`<span class="rst-status" data-tone="warning">`, `<span rst-status rst-tone="warning">`}, |
| 31 | {`<div class="rst-callout" data-tone="positive">`, `<div rst-callout rst-tone="positive">`}, |
| 32 | // Runtime state stays data-*. |
| 33 | {`<form class="rst-form" data-busy="false">`, `<form rst-form data-busy="false">`}, |
| 34 | {`<div class="rst-job" data-poll="/x">`, `<div rst-job data-poll="/x">`}, |
| 35 | // Not ours. |
| 36 | {`<svg class="icon">`, `<svg class="icon">`}, |
| 37 | {`<p class="flash flash-ok">`, `<p class="flash flash-ok">`}, |
| 38 | // The renames. |
| 39 | {`<div class="rst-form__foot">`, `<div rst-form-foot>`}, |
| 40 | {`<div class="rst-form-foot"><span class="rst-form-foot__note">x</span></div>`, |
| 41 | `<div rst-form-bar><span rst-form-bar-note>x</span></div>`}, |
| 42 | // The deletion, including the space it took with it. |
| 43 | {`<summary class="rst-btn rst-dropdown__summary">`, `<summary rst-btn>`}, |
| 44 | {`<summary class="rst-dropdown__summary">Filter</summary>`, `<summary>Filter</summary>`}, |
| 45 | // A Go interpreted string literal keeps its own quoting. |
| 46 | {`b.WriteString("<a class=\"rst-btn rst-btn--primary\">")`, |
| 47 | `b.WriteString("<a rst-btn=\"primary\">")`}, |
| 48 | {`b.WriteString("<div class=\"rst-dropdown__menu\">")`, |
| 49 | `b.WriteString("<div rst-dropdown-menu>")`}, |
| 50 | // Templated class lists: the conditional moves into the value. |
| 51 | {`<span class="rst-badge{{with .Tone}} rst-badge--{{.}}{{end}}">`, |
| 52 | `<span rst-badge{{with .Tone}}="{{.}}"{{end}}>`}, |
| 53 | {`<input class="rst-input{{if .Short}} rst-input--short{{end}}">`, |
| 54 | `<input rst-input{{if .Short}}="short"{{end}}>`}, |
| 55 | {`<button class="rst-btn{{if .Danger}} rst-btn--danger{{else}} rst-btn--primary{{end}}">`, |
| 56 | `<button rst-btn="{{if .Danger}}danger{{else}}primary{{end}}">`}, |
| 57 | {`<span class="rst-person__av{{if not .Initial}} rst-person__av--empty{{end}}">`, |
| 58 | `<span rst-person-av{{if not .Initial}}="empty"{{end}}>`}, |
| 59 | // A whole class attribute inside a conditional: the value has no |
| 60 | // action in it, so it is an ordinary literal list. |
| 61 | {`<legend{{if .Hidden}} class="rst-sr-only"{{end}}>`, `<legend{{if .Hidden}} class="rst-sr-only"{{end}}>`}, |
| 62 | {`<dd{{if .Mono}} class="rst-mono"{{end}}>`, `<dd{{if .Mono}} class="rst-mono"{{end}}>`}, |
| 63 | // A CSS selector is not markup: neither of these moves. |
| 64 | {`.rst-box { color: red }`, `.rst-box { color: red }`}, |
| 65 | {`.rst-status[data-tone="positive"] { color: red }`, `.rst-status[data-tone="positive"] { color: red }`}, |
| 66 | // A class attribute that opens a Go literal has no whitespace |
| 67 | // in front of it, and is still an attribute. |
| 68 | {"wantContains(t, body, `class=\"rst-field\"`)", "wantContains(t, body, `rst-field`)"}, |
| 69 | {"strings.Contains(got, `class=\"rst-status\" data-tone=\"positive\"`)", |
| 70 | "strings.Contains(got, `rst-status rst-tone=\"positive\"`)"}, |
| 71 | // data-class= and superclass= are not the class attribute. |
| 72 | {`<div data-class="rst-box">`, `<div data-class="rst-box">`}, |
| 73 | // A <details name> group is a value, not a class. |
| 74 | {`<details class="rst-row-menu" name="rst-menus">`, `<details rst-row-menu name="rst-menus">`}, |
| 75 | } { |
| 76 | got, notes := Rewrite([]byte(c.in)) |
| 77 | if string(got) != c.want { |
| 78 | t.Errorf("Rewrite(%q)\n got %q\nwant %q", c.in, got, c.want) |
| 79 | } |
| 80 | if len(notes) > 0 { |
| 81 | t.Errorf("Rewrite(%q) reported %v, but the shape is one it handles", c.in, notes) |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | // TestRewritingIsIdempotent is the property the release notes lean on: |
| 87 | // an app can run the tool twice, or run it over a tree half of which |
| 88 | // has already been done, and the second pass changes nothing. |
| 89 | func TestRewritingIsIdempotent(t *testing.T) { |
| 90 | src := `<div class="rst-card" style="--rst-cols: 2fr 32px"> |
| 91 | <div class="rst-lrow rst-lrow--head"><span class="rst-m-hide">Status</span></div> |
| 92 | <span class="rst-status" data-tone="positive">Paid</span> |
| 93 | <span class="rst-badge{{with .Tone}} rst-badge--{{.}}{{end}}">x</span> |
| 94 | <summary class="rst-btn rst-dropdown__summary">Filter</summary> |
| 95 | <div class="rst-form-foot"><span class="rst-form-foot__note">n</span></div> |
| 96 | </div>` |
| 97 | once, _ := Rewrite([]byte(src)) |
| 98 | twice, notes := Rewrite(once) |
| 99 | if string(once) != string(twice) { |
| 100 | t.Errorf("a second pass changed the file:\n%s\n---\n%s", once, twice) |
| 101 | } |
| 102 | if len(notes) > 0 { |
| 103 | t.Errorf("a second pass reported %v", notes) |
| 104 | } |
| 105 | // The only class= left is the utilities', which is the grammar. |
| 106 | for _, attr := range regexp.MustCompile(`class="([^"]*)"`).FindAllStringSubmatch(string(once), -1) { |
| 107 | for _, tok := range strings.Fields(attr[1]) { |
| 108 | if _, ok := Utilities[tok]; !ok && strings.HasPrefix(tok, "rst-") { |
| 109 | t.Errorf("a migrating class survived: %q in\n%s", tok, once) |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // TestAnUnreadableShapeIsReportedNotGuessed. A codemod that guesses is |
| 116 | // worse than one that stops: the wrong guess renders unstyled and looks |
| 117 | // like markup somebody wrote. |
| 118 | func TestAnUnreadableShapeIsReportedNotGuessed(t *testing.T) { |
| 119 | for _, in := range []string{ |
| 120 | `<div class="rst-box{{if .X}} rst-card{{end}}">`, // a second KIND, not a modifier |
| 121 | `<div class="{{if .X}}rst-btn--primary{{else}}rst-badge--positive{{end}}">`, // two kinds |
| 122 | `<div class="rst-box{{if .X}} something rst-box--wide{{end}}">`, // literal text beside it |
| 123 | } { |
| 124 | got, notes := Rewrite([]byte(in)) |
| 125 | if string(got) != in { |
| 126 | t.Errorf("Rewrite(%q) = %q: an unreadable shape must be left exactly as it was", in, got) |
| 127 | } |
| 128 | if len(notes) != 1 { |
| 129 | t.Errorf("Rewrite(%q) reported %d notes, want 1", in, len(notes)) |
| 130 | } |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | // TestSelectorTranslationIsTheSameGrammar. tokens.css's stage-1 twins |
| 135 | // and the markup flip have to mean the same thing by construction, so |
| 136 | // they are one package and this is the selector half of the table. |
| 137 | func TestSelectorTranslationIsTheSameGrammar(t *testing.T) { |
| 138 | for _, c := range []struct{ in, want string }{ |
| 139 | {".rst-box", "[rst-box]"}, |
| 140 | {".rst-box-head + .rst-box", "[rst-box-head] + [rst-box]"}, |
| 141 | {".rst-btn--primary:hover", `[rst-btn~="primary"]:hover`}, |
| 142 | {".rst-callout__body > p", "[rst-callout-body] > p"}, |
| 143 | {".rst-dtp__row--set .rst-dtp__label", `[rst-dtp-row~="set"] [rst-dtp-label]`}, |
| 144 | {".rst-person__av--empty", `[rst-person-av~="empty"]`}, |
| 145 | {`.rst-status[data-tone="positive"]`, `[rst-status][rst-tone~="positive"]`}, |
| 146 | {`.rst-btn[aria-busy="true"]`, `[rst-btn][aria-busy="true"]`}, |
| 147 | {".rst-mono", ".rst-mono"}, |
| 148 | {".rst-row-menu__panel .rst-danger:hover", "[rst-row-menu-panel] .rst-danger:hover"}, |
| 149 | {".rst-search .icon", "[rst-search] .icon"}, |
| 150 | {`.rst-row__lead[data-lead="positive"]`, `[rst-row-lead][data-lead="positive"]`}, |
| 151 | // Selector translation is the pure grammar: the markup renames |
| 152 | // have already happened in tokens.css itself, so a selector is |
| 153 | // never renamed under a rewrite's feet. |
| 154 | {".rst-form-foot", "[rst-form-foot]"}, |
| 155 | {".rst-form-bar .rst-form-bar__note", "[rst-form-bar] [rst-form-bar-note]"}, |
| 156 | } { |
| 157 | if got := Selector(c.in); got != c.want { |
| 158 | t.Errorf("Selector(%q) = %q, want %q", c.in, got, c.want) |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | // TestEveryQuotingHTMLAllows. The framework writes one shape; an app |
| 164 | // may write any of them. A tool that reads only the shape its own |
| 165 | // repository happens to use tells everyone else "nothing to do", and |
| 166 | // they find out at stage 3, when the class selectors are gone and |
| 167 | // nothing says why the page is unstyled. |
| 168 | func TestEveryQuotingHTMLAllows(t *testing.T) { |
| 169 | for _, c := range []struct{ in, want string }{ |
| 170 | {`<div class='rst-box'>`, `<div rst-box>`}, |
| 171 | {`<div class=rst-box>`, `<div rst-box>`}, |
| 172 | {`<div class = "rst-box">`, `<div rst-box>`}, |
| 173 | {`<div CLASS="rst-box">`, `<div rst-box>`}, |
| 174 | {`<div Class='rst-btn rst-btn--primary'>`, `<div rst-btn='primary'>`}, |
| 175 | {`<div class=rst-lrow--head>`, `<div rst-lrow="head">`}, |
| 176 | // A utility keeps class, and keeps the quoting it was written in. |
| 177 | {`<td class='rst-m-hide rst-cell-mut'>`, `<td class='rst-m-hide rst-cell-mut'>`}, |
| 178 | {`<td class=rst-mono>`, `<td class="rst-mono">`}, |
| 179 | // A JavaScript literal escapes its single quotes the way a Go |
| 180 | // one escapes its double quotes. |
| 181 | {`el.outerHTML = '<div class=\'rst-box\'>'`, `el.outerHTML = '<div rst-box>'`}, |
| 182 | } { |
| 183 | got, notes := Rewrite([]byte(c.in)) |
| 184 | if string(got) != c.want { |
| 185 | t.Errorf("Rewrite(%q)\n got %q\nwant %q", c.in, got, c.want) |
| 186 | } |
| 187 | if len(notes) > 0 { |
| 188 | t.Errorf("Rewrite(%q) reported %v, but the shape is one it handles", c.in, notes) |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | // TestThingsThatWearTheShapeOfAClassAttributeAndAreNot. Each of these |
| 194 | // must come out byte-identical and unreported: rewriting them would be |
| 195 | // the corruption, and reporting them would be noise a reader learns to |
| 196 | // ignore. |
| 197 | func TestThingsThatWearTheShapeOfAClassAttributeAndAreNot(t *testing.T) { |
| 198 | for _, in := range []string{ |
| 199 | `<div data-class="rst-box">`, |
| 200 | `if class == "rst-box" { }`, |
| 201 | `<a href="/x?class=rst-box&sort=new">link</a>`, |
| 202 | `<a href="/x?class=rst-box">link</a>`, |
| 203 | } { |
| 204 | got, notes := Rewrite([]byte(in)) |
| 205 | if string(got) != in { |
| 206 | t.Errorf("Rewrite(%q) = %q: it is not a class attribute and must not move", in, got) |
| 207 | } |
| 208 | if len(notes) > 0 { |
| 209 | t.Errorf("Rewrite(%q) reported %v: it is not markup, so there is nothing to tell anyone", in, notes) |
| 210 | } |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | // TestMarkupBuiltByConcatenationIsReportedNotCorrupted is the shape |
| 215 | // that used to come out as source that does not compile. The reader |
| 216 | // runs from class=\" to the next \" and lands in the middle of a Go |
| 217 | // expression; a value holding a quote or a backtick is not a class |
| 218 | // list, and saying so is the whole of the contract. |
| 219 | func TestMarkupBuiltByConcatenationIsReportedNotCorrupted(t *testing.T) { |
| 220 | for _, in := range []string{ |
| 221 | "return \"<div class=\\\"rst-\" + kind + \"\\\">x</div>\"", |
| 222 | "return `<a class=\"rst-btn rst-btn--` + tone + `\">go</a>`", |
| 223 | "var s = '<span class=\\'rst-badge rst-badge--' + tone + '\\'>'", |
| 224 | } { |
| 225 | got, notes := Rewrite([]byte(in)) |
| 226 | if string(got) != in { |
| 227 | t.Errorf("Rewrite(%q) = %q: markup built by concatenation must come back untouched", in, got) |
| 228 | } |
| 229 | if len(notes) != 1 { |
| 230 | t.Errorf("Rewrite(%q) reported %d notes, want 1 — silence here writes source that does not compile", in, len(notes)) |
| 231 | continue |
| 232 | } |
| 233 | if !strings.Contains(notes[0].Text, "not a class list") { |
| 234 | t.Errorf("the note does not say what is wrong: %s", notes[0]) |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | // An unquoted value with a template action in it is markup, and it is |
| 240 | // unreadable. Skipping it silently is the H3 failure wearing a |
| 241 | // different hat: a class attribute carrying the vocabulary that would |
| 242 | // simply never be migrated, with nothing ever saying so. |
| 243 | func TestAnUnquotedTemplatedValueIsReported(t *testing.T) { |
| 244 | const in = `<div class=rst-{{.Kind}}>x</div>` |
| 245 | got, notes := Rewrite([]byte(in)) |
| 246 | if string(got) != in { |
| 247 | t.Errorf("Rewrite(%q) = %q: it must not be guessed at", in, got) |
| 248 | } |
| 249 | if len(notes) != 1 || !strings.Contains(notes[0].Text, "cannot take apart") { |
| 250 | t.Errorf("an unquoted templated class value was not reported: %v", notes) |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | // TestEscapedMarkupIsReported: a documentation page showing |
| 255 | // class="rst-box" as source. Rewriting it would mean deciding |
| 256 | // what the escaping is for; naming it is the honest half. |
| 257 | func TestEscapedMarkupIsReported(t *testing.T) { |
| 258 | const in = `<p>Write <code>class="rst-box"</code> no longer.</p>` |
| 259 | got, notes := Rewrite([]byte(in)) |
| 260 | if string(got) != in { |
| 261 | t.Errorf("Rewrite(%q) = %q: escaped markup must not be rewritten", in, got) |
| 262 | } |
| 263 | if len(notes) != 1 || !strings.Contains(notes[0].Text, "escaped markup") { |
| 264 | t.Errorf("escaped markup carrying an rst- name was not reported: %v", notes) |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | // TestTheFenceIsTheOnlyOptOut. Every repository migrating its own |
| 269 | // documentation has a paragraph whose subject IS the old spelling, and |
| 270 | // without a way to say so the only opt-out is "do not run the tool on |
| 271 | // that file" — which is not one. |
| 272 | func TestTheFenceIsTheOnlyOptOut(t *testing.T) { |
| 273 | const in = "<div class=\"rst-box\">before</div>\n" + |
| 274 | "<!-- " + FenceBegin + " -->\n" + |
| 275 | "<div class=\"rst-box\" data-tone=\"positive\">inside</div>\n" + |
| 276 | "<!-- " + FenceEnd + " -->\n" + |
| 277 | "<div class=\"rst-box\">after</div>\n" |
| 278 | got, notes := Rewrite([]byte(in)) |
| 279 | want := "<div rst-box>before</div>\n" + |
| 280 | "<!-- " + FenceBegin + " -->\n" + |
| 281 | "<div class=\"rst-box\" data-tone=\"positive\">inside</div>\n" + |
| 282 | "<!-- " + FenceEnd + " -->\n" + |
| 283 | "<div rst-box>after</div>\n" |
| 284 | if string(got) != want { |
| 285 | t.Errorf("Rewrite\n got %q\nwant %q", got, want) |
| 286 | } |
| 287 | if len(notes) > 0 { |
| 288 | t.Errorf("a fenced region is a deliberate choice, not a finding: %v", notes) |
| 289 | } |
| 290 | // An unclosed fence runs to the end of the file, which is what a |
| 291 | // whole-file opt-out looks like written once at the top. |
| 292 | open := "<!-- " + FenceBegin + " -->\n<div class=\"rst-box\">x</div>\n" |
| 293 | if got, _ := Rewrite([]byte(open)); string(got) != open { |
| 294 | t.Errorf("an unclosed fence must reach the end of the file: %q", got) |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | // TestAFencedRegionProducesNoNote is the other half of |
| 299 | // TestTheFenceIsTheOnlyOptOut, and it was the half missing. The fence |
| 300 | // stopped the rewrite and did not stop the report, so a file that |
| 301 | // fences off a paragraph about the old spelling reported work forever |
| 302 | // and held the exit code at 3 while it did. |
| 303 | // |
| 304 | // This repository was the proof: internal/markup/rewrite.go and |
| 305 | // internal/markup/markup_test.go are fenced from line 1 and reported |
| 306 | // notes anyway, on every run, so `rastrillo markup .` could never exit |
| 307 | // 0 here on a clean tree. How many notes is not worth writing down — |
| 308 | // adding this very test added another one, which is how the count an |
| 309 | // earlier draft of this comment carried went stale before anyone read |
| 310 | // it. A report mode that always says there is work is what teaches a |
| 311 | // person to skip reading it and run --fix, which is the habit that |
| 312 | // made the Markdown defect expensive. |
| 313 | func TestAFencedRegionProducesNoNote(t *testing.T) { |
| 314 | // The same text TestEscapedMarkupIsReported asserts IS a note when |
| 315 | // it stands on its own — shared deliberately, so the two cannot |
| 316 | // drift into testing different things and agreeing. |
| 317 | const escaped = "<p>Write <code>class="rst-box"</code> no longer.</p>\n" |
| 318 | |
| 319 | // The data-tone attribute ahead of the fence is not decoration: the |
| 320 | // tone pass rewrites outside fences and shortens the text where it |
| 321 | // does, which is why the note pass computes the regions again |
| 322 | // rather than reusing the ones the tone pass was given. |
| 323 | in := "<span class=\"rst-status\" data-tone=\"ok\">live</span>\n" + |
| 324 | "<!-- " + FenceBegin + " -->\n" + escaped + "<!-- " + FenceEnd + " -->\n" |
| 325 | |
| 326 | got, notes := Rewrite([]byte(in)) |
| 327 | if len(notes) != 0 { |
| 328 | t.Errorf("a fenced paragraph about the old spelling is a decision already taken, not a finding: %v", notes) |
| 329 | } |
| 330 | if !strings.Contains(string(got), escaped) { |
| 331 | t.Errorf("the fenced paragraph was rewritten:\n%s", got) |
| 332 | } |
| 333 | // And the fence still ends where it says: markup outside it is |
| 334 | // migrated as usual, or "no notes" would just mean "did nothing". |
| 335 | if !strings.Contains(string(got), "<span rst-status rst-tone=\"ok\">") { |
| 336 | t.Errorf("the fence swallowed the markup outside it:\n%s", got) |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | // TestRespellIsTheGrammarWithoutTheMigration. Rewrite translates markup |
| 341 | // written before the flip, where rst-form-foot meant the sticky save |
| 342 | // bar. Respell translates markup written in today's class vocabulary, |
| 343 | // where it means the action row the form-foot partial emits. Running |
| 344 | // the wrong one over the wrong markup moves an element to a different |
| 345 | // rule and changes what renders, so the difference is pinned here. |
| 346 | func TestRespellIsTheGrammarWithoutTheMigration(t *testing.T) { |
| 347 | const in = `<div class="rst-form-foot"><span class="rst-form-foot__note">n</span></div>` |
| 348 | migrated, _ := Rewrite([]byte(in)) |
| 349 | if want := `<div rst-form-bar><span rst-form-bar-note>n</span></div>`; string(migrated) != want { |
| 350 | t.Errorf("Rewrite(%q) = %q, want %q", in, migrated, want) |
| 351 | } |
| 352 | respelled, _ := Respell([]byte(in)) |
| 353 | if want := `<div rst-form-foot><span rst-form-foot-note>n</span></div>`; string(respelled) != want { |
| 354 | t.Errorf("Respell(%q) = %q, want %q", in, respelled, want) |
| 355 | } |
| 356 | // Everything that is not a rename is the same translation in both. |
| 357 | const plain = `<a class="rst-btn rst-btn--primary">x</a>` |
| 358 | a, _ := Rewrite([]byte(plain)) |
| 359 | b, _ := Respell([]byte(plain)) |
| 360 | if string(a) != string(b) { |
| 361 | t.Errorf("Rewrite and Respell disagree on markup with no rename in it: %q vs %q", a, b) |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | // TestTheExemptionsAreDisjoint. A class cannot be both a utility that |
| 366 | // keeps its spelling and a name the flip renames or deletes; the three |
| 367 | // lists are the whole of what the grammar treats specially, so an |
| 368 | // overlap would make the translation depend on lookup order. |
| 369 | func TestTheExemptionsAreDisjoint(t *testing.T) { |
| 370 | for class := range Utilities { |
| 371 | if _, ok := Renamed[class]; ok { |
| 372 | t.Errorf("%q is both a utility and a rename", class) |
| 373 | } |
| 374 | if _, ok := Dropped[class]; ok { |
| 375 | t.Errorf("%q is both a utility and a deletion", class) |
| 376 | } |
| 377 | } |
| 378 | for class := range Renamed { |
| 379 | if _, ok := Dropped[class]; ok { |
| 380 | t.Errorf("%q is both a rename and a deletion", class) |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | |