Skip to content

Commit c965249

Browse files
authored
Improve misplaced standard attribute diagnostics (#66)
1 parent 586a5e3 commit c965249

3 files changed

Lines changed: 110 additions & 7 deletions

File tree

bdl-ts/src/linter/bdl.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,63 @@ Deno.test("lintBdl requires standard by default", async () => {
3939
assert(messages.includes("No BDL standard specified."));
4040
});
4141

42+
Deno.test("lintBdl suggests inner standard when standard is misplaced", async () => {
43+
const result = await lintBdlFinal({
44+
text: [
45+
"@ standard - conventional",
46+
"struct User {",
47+
" id: string,",
48+
"}",
49+
"",
50+
].join("\n"),
51+
});
52+
const misplacedStandardDiagnostic = result.diagnostics.find(
53+
(diag) => diag.code === "bdl/misplaced-standard",
54+
);
55+
assertStringIncludes(
56+
misplacedStandardDiagnostic?.message ?? "",
57+
'Did you mean "# standard - conventional"?',
58+
);
59+
assertEquals(misplacedStandardDiagnostic?.span, { start: 0, end: 25 });
60+
assertEquals(
61+
result.diagnostics.filter((diag) => diag.code === "bdl/unknown-attribute")
62+
.map((diag) => diag.message),
63+
[],
64+
);
65+
});
66+
67+
Deno.test("lintBdl only suggests inner standard for module-level attributes", async () => {
68+
const result = await lintBdlFinal({
69+
text: [
70+
"struct User {",
71+
" @ standard - conventional",
72+
" id: string,",
73+
"}",
74+
"",
75+
].join("\n"),
76+
standard: conventionalStandard,
77+
});
78+
const codes = result.diagnostics.map((diag) => diag.code);
79+
assert(codes.includes("bdl/missing-standard"));
80+
assert(!codes.includes("bdl/misplaced-standard"));
81+
});
82+
83+
Deno.test("lintBdl only suggests inner standard for outer attributes", async () => {
84+
const result = await lintBdlFinal({
85+
text: [
86+
"struct User {",
87+
" # standard - conventional",
88+
" id: string,",
89+
"}",
90+
"",
91+
].join("\n"),
92+
standard: conventionalStandard,
93+
});
94+
const codes = result.diagnostics.map((diag) => diag.code);
95+
assert(codes.includes("bdl/missing-standard"));
96+
assert(!codes.includes("bdl/misplaced-standard"));
97+
});
98+
4299
Deno.test("lintBdl validates standard id from bdlConfig", async () => {
43100
const result = await lintBdlFinal({
44101
text: [

bdl-ts/src/linter/bdl.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,25 @@ async function checkStandardId(
280280
);
281281

282282
if (!standardAttr) {
283+
const misplacedStandardAttr = findModuleLevelStandardAttribute(
284+
text,
285+
bdlAst,
286+
);
287+
if (misplacedStandardAttr) {
288+
diagnostics.push({
289+
code: "bdl/misplaced-standard",
290+
span: {
291+
start: misplacedStandardAttr.start,
292+
end: misplacedStandardAttr.end,
293+
},
294+
message:
295+
`Outer 'standard' attributes do not set the BDL standard.\nDid you mean "${
296+
formatStandardAttributeSuggestion(text, misplacedStandardAttr)
297+
}"?`,
298+
severity: "error",
299+
});
300+
return;
301+
}
283302
diagnostics.push({
284303
code: "bdl/missing-standard",
285304
span: { start: 0, end: 0 },
@@ -312,6 +331,29 @@ async function checkStandardId(
312331
return standardId;
313332
}
314333

334+
function findModuleLevelStandardAttribute(
335+
text: string,
336+
bdlAst: ast.BdlAst,
337+
): ast.Attribute | undefined {
338+
for (const statement of bdlAst.statements) {
339+
const standardAttr = statement.attributes.find(
340+
(attr) =>
341+
text[attr.start] === "@" && slice(text, attr.name) === "standard",
342+
);
343+
if (standardAttr) return standardAttr;
344+
}
345+
}
346+
347+
function formatStandardAttributeSuggestion(
348+
text: string,
349+
standardAttr: ast.Attribute,
350+
): string {
351+
const standardId = standardAttr.content
352+
? getAttributeContent(text, standardAttr).replace(/\s+/g, " ").trim()
353+
: "";
354+
return standardId ? `# standard - ${standardId}` : "# standard - <standard>";
355+
}
356+
315357
async function checkStandard(
316358
ctx: CheckContext,
317359
standardId: string | undefined,
@@ -383,6 +425,12 @@ function checkWrongAttributeNames(ctx: CheckContext): void {
383425
const diagnostics = result.diagnostics;
384426
const attributesBySlot = groupAttributesBySlot(bdlAst);
385427
const validAttributeKeys = {} as Record<AttributeSlot, Set<string>>;
428+
const hasModuleStandardAttr = bdlAst.attributes.some(
429+
(attr) => slice(text, attr.name) === "standard",
430+
);
431+
const misplacedStandardAttr = hasModuleStandardAttr
432+
? undefined
433+
: findModuleLevelStandardAttribute(text, bdlAst);
386434

387435
const attributeEntries = [
388436
...Object.entries(globalStandard.attributes || {}),
@@ -397,6 +445,7 @@ function checkWrongAttributeNames(ctx: CheckContext): void {
397445
const validAttributeKeySet = validAttributeKeys[slot as AttributeSlot];
398446
for (const attr of attrs as ast.Attribute[]) {
399447
const key = slice(text, attr.name);
448+
if (attr === misplacedStandardAttr) continue;
400449
if (validAttributeKeySet?.has(key)) continue;
401450
diagnostics.push({
402451
code: "bdl/unknown-attribute",

deno.lock

Lines changed: 4 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)