@intentius/chant 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bench.test.ts +1 -1
- package/src/cli/commands/doctor.ts +8 -3
- package/src/cli/commands/init.test.ts +44 -4
- package/src/cli/commands/init.ts +55 -23
- package/src/cli/commands/lint.ts +27 -13
- package/src/cli/handlers/init.ts +1 -0
- package/src/cli/lsp/server.ts +1 -1
- package/src/cli/main.ts +4 -0
- package/src/cli/mcp/server.test.ts +28 -2
- package/src/cli/mcp/tools/scaffold.ts +21 -3
- package/src/cli/registry.ts +1 -0
- package/src/cli/reporters/stylish.test.ts +212 -1
- package/src/cli/reporters/stylish.ts +133 -36
- package/src/codegen/docs-rules.test.ts +112 -0
- package/src/codegen/docs-rules.ts +129 -0
- package/src/codegen/docs.ts +3 -1
- package/src/codegen/generate-typescript.test.ts +64 -0
- package/src/codegen/generate-typescript.ts +13 -3
- package/src/codegen/package.ts +1 -1
- package/src/composite.test.ts +83 -16
- package/src/composite.ts +7 -5
- package/src/detectLexicon.test.ts +2 -2
- package/src/discovery/collect.test.ts +2 -2
- package/src/discovery/collect.ts +1 -1
- package/src/index.ts +1 -0
- package/src/lexicon-schema.ts +8 -0
- package/src/lexicon.ts +13 -1
- package/src/lint/declarative.ts +6 -0
- package/src/lint/engine.test.ts +287 -11
- package/src/lint/engine.ts +101 -23
- package/src/lint/rule-registry.test.ts +112 -0
- package/src/lint/rule-registry.ts +118 -0
- package/src/lint/rule.ts +8 -0
- package/src/lint/rules/cor017-composite-name-match.ts +2 -1
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
- package/src/lint/rules/declarable-naming-convention.ts +1 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
- package/src/lint/rules/evl004-spread-non-const.ts +1 -0
- package/src/lint/rules/evl005-resource-block-body.ts +1 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
- package/src/lint/rules/export-required.ts +1 -0
- package/src/lint/rules/file-declarable-limit.ts +1 -0
- package/src/lint/rules/flat-declarations.test.ts +8 -7
- package/src/lint/rules/flat-declarations.ts +2 -3
- package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
- package/src/lint/rules/no-redundant-type-import.ts +1 -0
- package/src/lint/rules/no-redundant-value-cast.ts +1 -0
- package/src/lint/rules/no-string-ref.ts +1 -0
- package/src/lint/rules/no-unused-declarable-import.ts +1 -0
- package/src/lint/rules/no-unused-declarable.test.ts +8 -0
- package/src/lint/rules/no-unused-declarable.ts +4 -0
- package/src/lint/rules/single-concern-file.ts +1 -0
- package/src/lsp/lexicon-providers.ts +7 -0
- package/src/lsp/types.ts +1 -0
- package/src/resource-attributes.test.ts +79 -0
- package/src/resource-attributes.ts +42 -0
- package/src/runtime.ts +4 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
2
|
import { formatStylish, formatSummary, formatJson, formatSarif } from "./stylish";
|
|
3
|
-
import type { LintDiagnostic } from "../../lint/rule";
|
|
3
|
+
import type { LintDiagnostic, LintRule } from "../../lint/rule";
|
|
4
4
|
|
|
5
5
|
describe("formatStylish", () => {
|
|
6
6
|
const originalNoColor = process.env.NO_COLOR;
|
|
@@ -279,4 +279,215 @@ describe("formatSarif", () => {
|
|
|
279
279
|
// Should have 2 unique rules
|
|
280
280
|
expect(parsed.runs[0].tool.driver.rules).toHaveLength(2);
|
|
281
281
|
});
|
|
282
|
+
|
|
283
|
+
test("enriches rules with descriptions when LintRule objects are provided", () => {
|
|
284
|
+
const diagnostics: LintDiagnostic[] = [
|
|
285
|
+
{
|
|
286
|
+
file: "test.ts",
|
|
287
|
+
line: 1,
|
|
288
|
+
column: 1,
|
|
289
|
+
ruleId: "COR001",
|
|
290
|
+
severity: "warning",
|
|
291
|
+
message: "Issue",
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const rules: LintRule[] = [
|
|
296
|
+
{
|
|
297
|
+
id: "COR001",
|
|
298
|
+
severity: "warning",
|
|
299
|
+
category: "style",
|
|
300
|
+
description: "No inline objects in Declarable constructors",
|
|
301
|
+
check: () => [],
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const result = formatSarif(diagnostics, rules);
|
|
306
|
+
const parsed = JSON.parse(result);
|
|
307
|
+
const sarifRule = parsed.runs[0].tool.driver.rules[0];
|
|
308
|
+
|
|
309
|
+
expect(sarifRule.shortDescription.text).toBe("No inline objects in Declarable constructors");
|
|
310
|
+
expect(sarifRule.fullDescription.text).toBe("No inline objects in Declarable constructors");
|
|
311
|
+
expect(sarifRule.helpUri).toContain("cor001");
|
|
312
|
+
expect(sarifRule.defaultConfiguration.level).toBe("warning");
|
|
313
|
+
expect(sarifRule.properties.category).toBe("style");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("includes fingerprints in results", () => {
|
|
317
|
+
const diagnostics: LintDiagnostic[] = [
|
|
318
|
+
{
|
|
319
|
+
file: "test.ts",
|
|
320
|
+
line: 10,
|
|
321
|
+
column: 5,
|
|
322
|
+
ruleId: "COR001",
|
|
323
|
+
severity: "warning",
|
|
324
|
+
message: "Something",
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
const result = formatSarif(diagnostics);
|
|
329
|
+
const parsed = JSON.parse(result);
|
|
330
|
+
|
|
331
|
+
expect(parsed.runs[0].results[0].fingerprints).toBeDefined();
|
|
332
|
+
expect(parsed.runs[0].results[0].fingerprints["chant/v1"]).toBe("COR001:test.ts:10:5");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("includes ruleIndex in results", () => {
|
|
336
|
+
const diagnostics: LintDiagnostic[] = [
|
|
337
|
+
{
|
|
338
|
+
file: "test.ts",
|
|
339
|
+
line: 1,
|
|
340
|
+
column: 1,
|
|
341
|
+
ruleId: "COR001",
|
|
342
|
+
severity: "warning",
|
|
343
|
+
message: "Issue",
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
const result = formatSarif(diagnostics);
|
|
348
|
+
const parsed = JSON.parse(result);
|
|
349
|
+
|
|
350
|
+
expect(parsed.runs[0].results[0].ruleIndex).toBe(0);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("includes endLine/endColumn when provided", () => {
|
|
354
|
+
const diagnostics: LintDiagnostic[] = [
|
|
355
|
+
{
|
|
356
|
+
file: "test.ts",
|
|
357
|
+
line: 10,
|
|
358
|
+
column: 5,
|
|
359
|
+
endLine: 12,
|
|
360
|
+
endColumn: 20,
|
|
361
|
+
ruleId: "COR001",
|
|
362
|
+
severity: "warning",
|
|
363
|
+
message: "Something",
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
const result = formatSarif(diagnostics);
|
|
368
|
+
const parsed = JSON.parse(result);
|
|
369
|
+
const region = parsed.runs[0].results[0].locations[0].physicalLocation.region;
|
|
370
|
+
|
|
371
|
+
expect(region.startLine).toBe(10);
|
|
372
|
+
expect(region.startColumn).toBe(5);
|
|
373
|
+
expect(region.endLine).toBe(12);
|
|
374
|
+
expect(region.endColumn).toBe(20);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("omits endLine/endColumn when not provided", () => {
|
|
378
|
+
const diagnostics: LintDiagnostic[] = [
|
|
379
|
+
{
|
|
380
|
+
file: "test.ts",
|
|
381
|
+
line: 10,
|
|
382
|
+
column: 5,
|
|
383
|
+
ruleId: "COR001",
|
|
384
|
+
severity: "warning",
|
|
385
|
+
message: "Something",
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
const result = formatSarif(diagnostics);
|
|
390
|
+
const parsed = JSON.parse(result);
|
|
391
|
+
const region = parsed.runs[0].results[0].locations[0].physicalLocation.region;
|
|
392
|
+
|
|
393
|
+
expect(region.endLine).toBeUndefined();
|
|
394
|
+
expect(region.endColumn).toBeUndefined();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("includes suppressed diagnostics with suppressions array", () => {
|
|
398
|
+
const diagnostics: LintDiagnostic[] = [
|
|
399
|
+
{
|
|
400
|
+
file: "test.ts",
|
|
401
|
+
line: 1,
|
|
402
|
+
column: 1,
|
|
403
|
+
ruleId: "COR001",
|
|
404
|
+
severity: "warning",
|
|
405
|
+
message: "Active issue",
|
|
406
|
+
},
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
const suppressed: Array<LintDiagnostic & { reason?: string }> = [
|
|
410
|
+
{
|
|
411
|
+
file: "test.ts",
|
|
412
|
+
line: 5,
|
|
413
|
+
column: 1,
|
|
414
|
+
ruleId: "COR001",
|
|
415
|
+
severity: "warning",
|
|
416
|
+
message: "Suppressed issue",
|
|
417
|
+
reason: "backwards compat",
|
|
418
|
+
},
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
const result = formatSarif(diagnostics, undefined, suppressed);
|
|
422
|
+
const parsed = JSON.parse(result);
|
|
423
|
+
|
|
424
|
+
// Should have 2 results total (1 active + 1 suppressed)
|
|
425
|
+
expect(parsed.runs[0].results).toHaveLength(2);
|
|
426
|
+
|
|
427
|
+
// First result: no suppressions
|
|
428
|
+
expect(parsed.runs[0].results[0].suppressions).toBeUndefined();
|
|
429
|
+
|
|
430
|
+
// Second result: has suppressions
|
|
431
|
+
expect(parsed.runs[0].results[1].suppressions).toHaveLength(1);
|
|
432
|
+
expect(parsed.runs[0].results[1].suppressions[0].kind).toBe("inSource");
|
|
433
|
+
expect(parsed.runs[0].results[1].suppressions[0].justification).toBe("backwards compat");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("suppressed diagnostics without reason omit justification", () => {
|
|
437
|
+
const suppressed: Array<LintDiagnostic & { reason?: string }> = [
|
|
438
|
+
{
|
|
439
|
+
file: "test.ts",
|
|
440
|
+
line: 5,
|
|
441
|
+
column: 1,
|
|
442
|
+
ruleId: "COR001",
|
|
443
|
+
severity: "warning",
|
|
444
|
+
message: "Suppressed issue",
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const result = formatSarif([], undefined, suppressed);
|
|
449
|
+
const parsed = JSON.parse(result);
|
|
450
|
+
|
|
451
|
+
expect(parsed.runs[0].results[0].suppressions[0].kind).toBe("inSource");
|
|
452
|
+
expect(parsed.runs[0].results[0].suppressions[0].justification).toBeUndefined();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("falls back to rule ID when no description available", () => {
|
|
456
|
+
const diagnostics: LintDiagnostic[] = [
|
|
457
|
+
{
|
|
458
|
+
file: "test.ts",
|
|
459
|
+
line: 1,
|
|
460
|
+
column: 1,
|
|
461
|
+
ruleId: "UNKNOWN",
|
|
462
|
+
severity: "warning",
|
|
463
|
+
message: "Issue",
|
|
464
|
+
},
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
const result = formatSarif(diagnostics);
|
|
468
|
+
const parsed = JSON.parse(result);
|
|
469
|
+
const sarifRule = parsed.runs[0].tool.driver.rules[0];
|
|
470
|
+
|
|
471
|
+
expect(sarifRule.shortDescription.text).toBe("UNKNOWN");
|
|
472
|
+
expect(sarifRule.helpUri).toContain("unknown");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("converts absolute file paths to file:// URIs", () => {
|
|
476
|
+
const diagnostics: LintDiagnostic[] = [
|
|
477
|
+
{
|
|
478
|
+
file: "/Users/test/project/test.ts",
|
|
479
|
+
line: 1,
|
|
480
|
+
column: 1,
|
|
481
|
+
ruleId: "COR001",
|
|
482
|
+
severity: "warning",
|
|
483
|
+
message: "Issue",
|
|
484
|
+
},
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
const result = formatSarif(diagnostics);
|
|
488
|
+
const parsed = JSON.parse(result);
|
|
489
|
+
const uri = parsed.runs[0].results[0].locations[0].physicalLocation.artifactLocation.uri;
|
|
490
|
+
|
|
491
|
+
expect(uri).toMatch(/^file:\/\//);
|
|
492
|
+
});
|
|
282
493
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { LintDiagnostic } from "../../lint/rule";
|
|
1
|
+
import type { LintDiagnostic, LintRule, Severity } from "../../lint/rule";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* ANSI color codes
|
|
@@ -121,10 +122,90 @@ export function formatJson(diagnostics: LintDiagnostic[]): string {
|
|
|
121
122
|
return JSON.stringify(diagnostics, null, 2);
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Map lint severity to SARIF level
|
|
127
|
+
*/
|
|
128
|
+
function mapSeverity(severity: Severity): "error" | "warning" | "note" {
|
|
129
|
+
if (severity === "error") return "error";
|
|
130
|
+
if (severity === "warning") return "warning";
|
|
131
|
+
return "note";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build a SARIF region object, only including endLine/endColumn when available
|
|
136
|
+
*/
|
|
137
|
+
function buildRegion(diag: LintDiagnostic): Record<string, number> {
|
|
138
|
+
const region: Record<string, number> = {
|
|
139
|
+
startLine: diag.line,
|
|
140
|
+
startColumn: diag.column,
|
|
141
|
+
};
|
|
142
|
+
if (diag.endLine !== undefined) region.endLine = diag.endLine;
|
|
143
|
+
if (diag.endColumn !== undefined) region.endColumn = diag.endColumn;
|
|
144
|
+
return region;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build a SARIF result from a diagnostic
|
|
149
|
+
*/
|
|
150
|
+
function buildSarifResult(diag: LintDiagnostic, ruleIndex: Map<string, number>) {
|
|
151
|
+
const result: Record<string, unknown> = {
|
|
152
|
+
ruleId: diag.ruleId,
|
|
153
|
+
ruleIndex: ruleIndex.get(diag.ruleId),
|
|
154
|
+
level: mapSeverity(diag.severity),
|
|
155
|
+
message: { text: diag.message },
|
|
156
|
+
locations: [
|
|
157
|
+
{
|
|
158
|
+
physicalLocation: {
|
|
159
|
+
artifactLocation: {
|
|
160
|
+
uri: diag.file.startsWith("/") ? pathToFileURL(diag.file).href : diag.file,
|
|
161
|
+
},
|
|
162
|
+
region: buildRegion(diag),
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
fingerprints: {
|
|
167
|
+
"chant/v1": `${diag.ruleId}:${diag.file}:${diag.line}:${diag.column}`,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
124
173
|
/**
|
|
125
174
|
* Format diagnostics as SARIF (Static Analysis Results Interchange Format)
|
|
175
|
+
*
|
|
176
|
+
* @param diagnostics - Active (non-suppressed) diagnostics
|
|
177
|
+
* @param rules - Full list of loaded LintRules for rich metadata (optional)
|
|
178
|
+
* @param suppressed - Suppressed diagnostics with optional reason (optional)
|
|
179
|
+
* @param version - Tool version string (optional, defaults to "0.1.0")
|
|
126
180
|
*/
|
|
127
|
-
export function formatSarif(
|
|
181
|
+
export function formatSarif(
|
|
182
|
+
diagnostics: LintDiagnostic[],
|
|
183
|
+
rules?: LintRule[],
|
|
184
|
+
suppressed?: Array<LintDiagnostic & { reason?: string }>,
|
|
185
|
+
version?: string,
|
|
186
|
+
): string {
|
|
187
|
+
// Build rule metadata from LintRule objects when available, otherwise from diagnostics
|
|
188
|
+
const { sarifRules, ruleIndex } = buildRuleMetadata(diagnostics, suppressed ?? [], rules);
|
|
189
|
+
|
|
190
|
+
// Build active results
|
|
191
|
+
const results: Record<string, unknown>[] = diagnostics.map((diag) =>
|
|
192
|
+
buildSarifResult(diag, ruleIndex),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Append suppressed results with SARIF suppressions
|
|
196
|
+
if (suppressed) {
|
|
197
|
+
for (const diag of suppressed) {
|
|
198
|
+
const result = buildSarifResult(diag, ruleIndex);
|
|
199
|
+
result.suppressions = [
|
|
200
|
+
{
|
|
201
|
+
kind: "inSource",
|
|
202
|
+
...(diag.reason ? { justification: diag.reason } : {}),
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
results.push(result);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
128
209
|
const sarif = {
|
|
129
210
|
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
130
211
|
version: "2.1.0",
|
|
@@ -133,31 +214,12 @@ export function formatSarif(diagnostics: LintDiagnostic[]): string {
|
|
|
133
214
|
tool: {
|
|
134
215
|
driver: {
|
|
135
216
|
name: "chant",
|
|
136
|
-
version: "0.1.0",
|
|
217
|
+
version: version ?? "0.1.0",
|
|
137
218
|
informationUri: "https://chant.dev",
|
|
138
|
-
rules:
|
|
219
|
+
rules: sarifRules,
|
|
139
220
|
},
|
|
140
221
|
},
|
|
141
|
-
results
|
|
142
|
-
ruleId: diag.ruleId,
|
|
143
|
-
level: diag.severity === "error" ? "error" : diag.severity === "warning" ? "warning" : "note",
|
|
144
|
-
message: {
|
|
145
|
-
text: diag.message,
|
|
146
|
-
},
|
|
147
|
-
locations: [
|
|
148
|
-
{
|
|
149
|
-
physicalLocation: {
|
|
150
|
-
artifactLocation: {
|
|
151
|
-
uri: diag.file,
|
|
152
|
-
},
|
|
153
|
-
region: {
|
|
154
|
-
startLine: diag.line,
|
|
155
|
-
startColumn: diag.column,
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
})),
|
|
222
|
+
results,
|
|
161
223
|
},
|
|
162
224
|
],
|
|
163
225
|
};
|
|
@@ -166,21 +228,56 @@ export function formatSarif(diagnostics: LintDiagnostic[]): string {
|
|
|
166
228
|
}
|
|
167
229
|
|
|
168
230
|
/**
|
|
169
|
-
*
|
|
231
|
+
* Build SARIF rule metadata from loaded LintRules and/or diagnostics.
|
|
232
|
+
* Returns both the rules array and a ruleId→index map for result references.
|
|
170
233
|
*/
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
234
|
+
function buildRuleMetadata(
|
|
235
|
+
diagnostics: LintDiagnostic[],
|
|
236
|
+
suppressed: LintDiagnostic[],
|
|
237
|
+
rules?: LintRule[],
|
|
238
|
+
): {
|
|
239
|
+
sarifRules: Record<string, unknown>[];
|
|
240
|
+
ruleIndex: Map<string, number>;
|
|
241
|
+
} {
|
|
242
|
+
// Build a lookup from rule objects
|
|
243
|
+
const ruleMap = new Map<string, LintRule>();
|
|
244
|
+
if (rules) {
|
|
245
|
+
for (const r of rules) {
|
|
246
|
+
ruleMap.set(r.id, r);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
174
249
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
250
|
+
// Collect all unique rule IDs from diagnostics + suppressed
|
|
251
|
+
const seen = new Set<string>();
|
|
252
|
+
const orderedIds: string[] = [];
|
|
253
|
+
for (const diag of [...diagnostics, ...suppressed]) {
|
|
254
|
+
if (!seen.has(diag.ruleId)) {
|
|
255
|
+
seen.add(diag.ruleId);
|
|
256
|
+
orderedIds.push(diag.ruleId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const sarifRules: Record<string, unknown>[] = [];
|
|
261
|
+
const ruleIndex = new Map<string, number>();
|
|
262
|
+
|
|
263
|
+
for (const id of orderedIds) {
|
|
264
|
+
const rule = ruleMap.get(id);
|
|
265
|
+
const descText = rule?.description || id;
|
|
266
|
+
const entry: Record<string, unknown> = {
|
|
267
|
+
id,
|
|
268
|
+
shortDescription: { text: descText },
|
|
269
|
+
fullDescription: { text: descText },
|
|
270
|
+
helpUri: rule?.helpUri || `https://chant.dev/lint-rules/${id.toLowerCase()}`,
|
|
271
|
+
defaultConfiguration: {
|
|
272
|
+
level: mapSeverity(rule?.severity ?? "warning"),
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
if (rule?.category) {
|
|
276
|
+
entry.properties = { category: rule.category };
|
|
182
277
|
}
|
|
278
|
+
ruleIndex.set(id, sarifRules.length);
|
|
279
|
+
sarifRules.push(entry);
|
|
183
280
|
}
|
|
184
281
|
|
|
185
|
-
return
|
|
282
|
+
return { sarifRules, ruleIndex };
|
|
186
283
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { generateRuleCatalog, generateRuleDetailPage } from "./docs-rules";
|
|
3
|
+
import type { RuleEntry } from "../lint/rule-registry";
|
|
4
|
+
|
|
5
|
+
const mockEntries: RuleEntry[] = [
|
|
6
|
+
{
|
|
7
|
+
id: "COR001",
|
|
8
|
+
description: "No inline objects in Declarable constructors",
|
|
9
|
+
category: "style",
|
|
10
|
+
defaultSeverity: "warning",
|
|
11
|
+
source: "core",
|
|
12
|
+
phase: "pre-synth",
|
|
13
|
+
hasAutoFix: false,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "WAW018",
|
|
17
|
+
description: "S3 bucket missing public access block",
|
|
18
|
+
category: "security",
|
|
19
|
+
defaultSeverity: "error",
|
|
20
|
+
source: "aws",
|
|
21
|
+
phase: "post-synth",
|
|
22
|
+
hasAutoFix: false,
|
|
23
|
+
helpUri: "https://chant.dev/lint-rules/waw018",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "COR008",
|
|
27
|
+
description: "Export required for declarable instances",
|
|
28
|
+
category: "correctness",
|
|
29
|
+
defaultSeverity: "warning",
|
|
30
|
+
source: "core",
|
|
31
|
+
phase: "pre-synth",
|
|
32
|
+
hasAutoFix: true,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe("generateRuleCatalog", () => {
|
|
37
|
+
test("generates valid MDX with frontmatter", () => {
|
|
38
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
39
|
+
|
|
40
|
+
expect(mdx).toContain("---");
|
|
41
|
+
expect(mdx).toContain('title: "Rule Reference"');
|
|
42
|
+
expect(mdx).toContain("**3** rules");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("groups rules by category", () => {
|
|
46
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
47
|
+
|
|
48
|
+
expect(mdx).toContain("## Correctness");
|
|
49
|
+
expect(mdx).toContain("## Security");
|
|
50
|
+
expect(mdx).toContain("## Style");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("includes table headers", () => {
|
|
54
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
55
|
+
|
|
56
|
+
expect(mdx).toContain("| ID | Description | Severity | Phase | Fix |");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("renders rule entries with correct data", () => {
|
|
60
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
61
|
+
|
|
62
|
+
expect(mdx).toContain("COR001");
|
|
63
|
+
expect(mdx).toContain("No inline objects");
|
|
64
|
+
expect(mdx).toContain("WAW018");
|
|
65
|
+
expect(mdx).toContain("public access");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("marks auto-fix rules", () => {
|
|
69
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
70
|
+
const lines = mdx.split("\n");
|
|
71
|
+
const cor008Line = lines.find((l) => l.includes("COR008"));
|
|
72
|
+
expect(cor008Line).toContain("Yes");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("renders helpUri as link", () => {
|
|
76
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
77
|
+
expect(mdx).toContain("[`WAW018`](https://chant.dev/lint-rules/waw018)");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("generateRuleDetailPage", () => {
|
|
82
|
+
test("generates valid MDX with frontmatter", () => {
|
|
83
|
+
const mdx = generateRuleDetailPage(mockEntries[0]);
|
|
84
|
+
|
|
85
|
+
expect(mdx).toContain("---");
|
|
86
|
+
expect(mdx).toContain("COR001");
|
|
87
|
+
expect(mdx).toContain("No inline objects");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("includes metadata table", () => {
|
|
91
|
+
const mdx = generateRuleDetailPage(mockEntries[0]);
|
|
92
|
+
|
|
93
|
+
expect(mdx).toContain("| **Severity** | warning |");
|
|
94
|
+
expect(mdx).toContain("| **Category** | style |");
|
|
95
|
+
expect(mdx).toContain("| **Phase** | pre-synth |");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("includes configuration example", () => {
|
|
99
|
+
const mdx = generateRuleDetailPage(mockEntries[0]);
|
|
100
|
+
|
|
101
|
+
expect(mdx).toContain("## Configuration");
|
|
102
|
+
expect(mdx).toContain('"COR001"');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("includes disable syntax", () => {
|
|
106
|
+
const mdx = generateRuleDetailPage(mockEntries[0]);
|
|
107
|
+
|
|
108
|
+
expect(mdx).toContain("## Disabling");
|
|
109
|
+
expect(mdx).toContain("chant-disable COR001");
|
|
110
|
+
expect(mdx).toContain("chant-disable-next-line COR001 -- reason");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule documentation auto-generation.
|
|
3
|
+
*
|
|
4
|
+
* Generates MDX pages from a RuleEntry registry:
|
|
5
|
+
* - A catalog page with all rules grouped by category
|
|
6
|
+
* - Per-rule detail pages with configuration examples and disable syntax
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RuleEntry } from "../lint/rule-registry";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a rule catalog MDX page with all rules grouped by category.
|
|
13
|
+
*/
|
|
14
|
+
export function generateRuleCatalog(
|
|
15
|
+
entries: RuleEntry[],
|
|
16
|
+
displayName = "chant",
|
|
17
|
+
): string {
|
|
18
|
+
const lines: string[] = [
|
|
19
|
+
"---",
|
|
20
|
+
`title: "Rule Reference"`,
|
|
21
|
+
`description: "Complete reference of all lint rules and post-synth checks"`,
|
|
22
|
+
"---",
|
|
23
|
+
"",
|
|
24
|
+
`This page lists all **${entries.length}** rules available in ${displayName}.`,
|
|
25
|
+
"",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Group by category
|
|
29
|
+
const byCategory = new Map<string, RuleEntry[]>();
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const cat = entry.category;
|
|
32
|
+
const existing = byCategory.get(cat) ?? [];
|
|
33
|
+
existing.push(entry);
|
|
34
|
+
byCategory.set(cat, existing);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Render each category
|
|
38
|
+
const categoryOrder = ["correctness", "security", "style", "performance"];
|
|
39
|
+
const sortedCategories = [...byCategory.keys()].sort(
|
|
40
|
+
(a, b) => (categoryOrder.indexOf(a) ?? 99) - (categoryOrder.indexOf(b) ?? 99),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
for (const cat of sortedCategories) {
|
|
44
|
+
const catEntries = byCategory.get(cat)!;
|
|
45
|
+
const title = cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
46
|
+
|
|
47
|
+
lines.push(
|
|
48
|
+
`## ${title}`,
|
|
49
|
+
"",
|
|
50
|
+
"| ID | Description | Severity | Phase | Fix |",
|
|
51
|
+
"|----|-------------|----------|-------|-----|",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
for (const entry of catEntries.sort((a, b) => a.id.localeCompare(b.id))) {
|
|
55
|
+
const fix = entry.hasAutoFix ? "Yes" : "";
|
|
56
|
+
const link = entry.helpUri
|
|
57
|
+
? `[\`${entry.id}\`](${entry.helpUri})`
|
|
58
|
+
: `\`${entry.id}\``;
|
|
59
|
+
lines.push(
|
|
60
|
+
`| ${link} | ${entry.description} | ${entry.defaultSeverity} | ${entry.phase} | ${fix} |`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
lines.push("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate a per-rule detail MDX page.
|
|
72
|
+
*/
|
|
73
|
+
export function generateRuleDetailPage(entry: RuleEntry): string {
|
|
74
|
+
const lines: string[] = [
|
|
75
|
+
"---",
|
|
76
|
+
`title: "${entry.id}: ${entry.description}"`,
|
|
77
|
+
`description: "${entry.description}"`,
|
|
78
|
+
"---",
|
|
79
|
+
"",
|
|
80
|
+
`# ${entry.id}`,
|
|
81
|
+
"",
|
|
82
|
+
entry.description,
|
|
83
|
+
"",
|
|
84
|
+
"| Property | Value |",
|
|
85
|
+
"|----------|-------|",
|
|
86
|
+
`| **ID** | \`${entry.id}\` |`,
|
|
87
|
+
`| **Severity** | ${entry.defaultSeverity} |`,
|
|
88
|
+
`| **Category** | ${entry.category} |`,
|
|
89
|
+
`| **Phase** | ${entry.phase} |`,
|
|
90
|
+
`| **Source** | ${entry.source} |`,
|
|
91
|
+
`| **Auto-fix** | ${entry.hasAutoFix ? "Yes" : "No"} |`,
|
|
92
|
+
"",
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Configuration example
|
|
96
|
+
lines.push(
|
|
97
|
+
"## Configuration",
|
|
98
|
+
"",
|
|
99
|
+
"Override severity in your `chant.config.ts`:",
|
|
100
|
+
"",
|
|
101
|
+
"```ts",
|
|
102
|
+
"// chant.config.ts",
|
|
103
|
+
`rules: {`,
|
|
104
|
+
` "${entry.id}": "warning", // or "error", "info", "off"`,
|
|
105
|
+
`}`,
|
|
106
|
+
"```",
|
|
107
|
+
"",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Disable syntax
|
|
111
|
+
lines.push(
|
|
112
|
+
"## Disabling",
|
|
113
|
+
"",
|
|
114
|
+
"Suppress this rule with inline comments:",
|
|
115
|
+
"",
|
|
116
|
+
"```ts",
|
|
117
|
+
`// chant-disable ${entry.id}`,
|
|
118
|
+
`// ... entire file suppressed for ${entry.id}`,
|
|
119
|
+
"",
|
|
120
|
+
`const x = 1; // chant-disable-line ${entry.id}`,
|
|
121
|
+
"",
|
|
122
|
+
`// chant-disable-next-line ${entry.id} -- reason for suppression`,
|
|
123
|
+
`const y = 2;`,
|
|
124
|
+
"```",
|
|
125
|
+
"",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface DocsConfig {
|
|
|
32
32
|
/** Custom sections to append to overview page */
|
|
33
33
|
extraSections?: Array<{ title: string; content: string }>;
|
|
34
34
|
/** Standalone pages added to the sidebar after Overview */
|
|
35
|
-
extraPages?: Array<{ slug: string; title: string; description?: string; content: string }>;
|
|
35
|
+
extraPages?: Array<{ slug: string; title: string; description?: string; content: string; sidebar?: boolean }>;
|
|
36
36
|
/** Slugs of auto-generated pages to suppress (e.g. "pseudo-parameters") */
|
|
37
37
|
suppressPages?: string[];
|
|
38
38
|
/** Source directory for scanning rule files (defaults to srcDir sibling of distDir) */
|
|
@@ -385,6 +385,7 @@ function buildSidebar(
|
|
|
385
385
|
// Extra pages from lexicon config (appear after Overview)
|
|
386
386
|
if (config.extraPages) {
|
|
387
387
|
for (const page of config.extraPages) {
|
|
388
|
+
if (page.sidebar === false) continue;
|
|
388
389
|
items.push({ label: page.title, slug: page.slug });
|
|
389
390
|
}
|
|
390
391
|
}
|
|
@@ -447,6 +448,7 @@ function generateOverview(
|
|
|
447
448
|
// Extra pages listed first in reference links
|
|
448
449
|
if (config.extraPages && config.extraPages.length > 0) {
|
|
449
450
|
for (const page of config.extraPages) {
|
|
451
|
+
if (page.sidebar === false) continue;
|
|
450
452
|
lines.push(`- [${page.title}](./${page.slug})`);
|
|
451
453
|
}
|
|
452
454
|
}
|