Skip to content

Commit c1d6f95

Browse files
donislawdevclaude
andcommitted
recipe: a refusal about a property carries the reason and what to do instead
The first version printed a bare dash where the reason should be, because an unknown property key had a subject and a sentence and nothing in between - and the guard that checks a refusal has all its parts was asking about one recipe rather than about every kind of refusal. It asks about every kind now, which is what caught this. The names a format takes are said once, in what to do instead, rather than twice - the sentence and the list read as one thought repeated. Error stays unchanged to the character, because it is what the one-target path from the command line flags prints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9cb9847 commit c1d6f95

3 files changed

Lines changed: 62 additions & 5 deletions

File tree

‎internal/format/format.go‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,12 +335,36 @@ type UnknownPropertyError struct {
335335
// about a declared setting that still landed at the foot of the form.
336336
func (e *UnknownPropertyError) AboutSetting() string { return e.Key }
337337

338-
func (e *UnknownPropertyError) Error() string {
338+
// Why this is refused, for a report that keeps the four parts of D6 apart.
339+
func (e *UnknownPropertyError) Why() string {
340+
return "a format takes only the settings it declares, and one it does not know would be dropped on the way"
341+
}
342+
343+
// Instead is what to do about it, named from the declaration.
344+
func (e *UnknownPropertyError) Instead() string {
345+
if len(e.Known) == 0 {
346+
return "remove the line"
347+
}
348+
return "use one of: " + strings.Join(e.Known, ", ")
349+
}
350+
351+
// What happened, without the list of names. Kept apart from Error so a report
352+
// with four parts does not print the names twice - once in the sentence and
353+
// again in what to do instead.
354+
func (e *UnknownPropertyError) What() string {
339355
if len(e.Known) == 0 {
340356
return fmt.Sprintf("%s takes no properties, so %q is not one of them", e.Format, e.Key)
341357
}
342-
return fmt.Sprintf("%s does not have a property called %q. It takes: %s",
343-
e.Format, e.Key, strings.Join(e.Known, ", "))
358+
return fmt.Sprintf("%s does not have a property called %q", e.Format, e.Key)
359+
}
360+
361+
// Error is the whole thing in one sentence, unchanged to the character - it is
362+
// what the one-target path from the command line flags prints.
363+
func (e *UnknownPropertyError) Error() string {
364+
if len(e.Known) == 0 {
365+
return e.What()
366+
}
367+
return e.What() + ". It takes: " + strings.Join(e.Known, ", ")
344368
}
345369

346370
// PropertyValueError is a declared key given a value the declaration forbids.

‎internal/guard/refusaladdress_test.go‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,18 @@ targets:
261261
}
262262
got = append(got, p.At)
263263
assertAddressResolves(t, known, p.At, p.What)
264+
// And it carries the parts a refusal in this tool is made of -
265+
// what happened, why, what to do instead (D6). Asked of every
266+
// case here rather than of one recipe, because a path that
267+
// leaves one of them empty prints a bare dash where the reason
268+
// should be, and that is exactly what the first version of the
269+
// property check did on 2026-08-25.
270+
if p.Why == "" || p.Fix == "" {
271+
t.Errorf("the refusal %q at %s has no %s.\n"+
272+
"A refusal here says what happened, why, and what to do instead - a missing\n"+
273+
"part prints as a bare dash rather than as nothing.",
274+
p.What, p.At, missingPart(p.Why, p.Fix))
275+
}
264276
}
265277

266278
if strings.Join(got, ", ") != strings.Join(c.want, ", ") {
@@ -448,3 +460,16 @@ func assertAddressResolves(t *testing.T, known map[string]bool, at, what string)
448460
"nothing can resolve is worse than none: a surface would look for a box\n"+
449461
"under that name, find nothing, and say nothing about it.", at, what, key)
450462
}
463+
464+
// missingPart names which half of a refusal is absent, so the failure says it
465+
// rather than leaving somebody to compare two empty strings.
466+
func missingPart(why, fix string) string {
467+
switch {
468+
case why == "" && fix == "":
469+
return "why and no fix"
470+
case why == "":
471+
return "why"
472+
default:
473+
return "fix"
474+
}
475+
}

‎internal/recipe/target.go‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,15 @@ func askTheFormat(p *problems, where spot, formatID string, stated map[string]st
527527
core.InTheWordsOf(value.Reason, value.Key), value.Instead)
528528
continue
529529
}
530-
p.add(at, fmt.Sprintf("%s: %s", where, bad.Error()), "",
531-
"use one of the properties the format declares")
530+
var unknown *format.UnknownPropertyError
531+
if errors.As(bad, &unknown) {
532+
p.add(at, fmt.Sprintf("%s: %s", where, unknown.What()),
533+
unknown.Why(), unknown.Instead())
534+
continue
535+
}
536+
// A kind of problem this does not know the shape of. It still names its
537+
// box, and the sentence still arrives whole - an empty why would print
538+
// as a bare dash, which is what the first version of this did.
539+
p.add(at, fmt.Sprintf("%s: %s", where, bad.Error()), "", "")
532540
}
533541
}

0 commit comments

Comments
 (0)