@intentius/chant 0.0.5 → 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/bin/chant +20 -0
- package/package.json +18 -17
- package/src/bench.test.ts +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
- package/src/cli/commands/build.ts +1 -2
- package/src/cli/commands/doctor.ts +8 -3
- package/src/cli/commands/import.ts +2 -2
- package/src/cli/commands/init-lexicon.test.ts +0 -3
- package/src/cli/commands/init-lexicon.ts +1 -79
- package/src/cli/commands/init.test.ts +44 -4
- package/src/cli/commands/init.ts +69 -26
- package/src/cli/commands/lint.ts +27 -13
- package/src/cli/commands/list.ts +2 -2
- package/src/cli/commands/update.ts +5 -3
- package/src/cli/conflict-check.test.ts +0 -1
- package/src/cli/handlers/dev.ts +1 -9
- package/src/cli/handlers/init.ts +1 -0
- package/src/cli/lsp/server.ts +1 -1
- package/src/cli/main.ts +17 -3
- package/src/cli/mcp/server.test.ts +233 -4
- package/src/cli/mcp/server.ts +6 -0
- package/src/cli/mcp/tools/explain.ts +134 -0
- package/src/cli/mcp/tools/scaffold.ts +125 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- 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-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +2 -3
- package/src/codegen/generate-typescript.test.ts +70 -6
- package/src/codegen/generate-typescript.ts +15 -9
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/package.ts +1 -1
- package/src/codegen/typecheck.ts +6 -11
- package/src/composite.test.ts +83 -16
- package/src/composite.ts +7 -5
- package/src/config.ts +4 -0
- 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 +2 -1
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon-schema.ts +8 -0
- package/src/lexicon.ts +15 -7
- package/src/lint/config.ts +8 -6
- 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-adapter.ts +158 -0
- package/src/runtime.ts +4 -3
- package/src/serializer-walker.test.ts +0 -9
- package/src/serializer-walker.ts +1 -3
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
- package/src/codegen/case.test.ts +0 -30
- package/src/codegen/case.ts +0 -11
- package/src/codegen/rollback.test.ts +0 -92
- package/src/codegen/rollback.ts +0 -115
package/src/lint/engine.test.ts
CHANGED
|
@@ -29,7 +29,7 @@ describe("runLint", () => {
|
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
const diagnostics = await runLint([testFile], [mockRule]);
|
|
32
|
+
const { diagnostics } = await runLint([testFile], [mockRule]);
|
|
33
33
|
|
|
34
34
|
expect(diagnostics).toHaveLength(1);
|
|
35
35
|
expect(diagnostics[0].ruleId).toBe("test-rule");
|
|
@@ -64,7 +64,7 @@ const x = 1;`
|
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
const diagnostics = await runLint([testFile], [mockRule]);
|
|
67
|
+
const { diagnostics } = await runLint([testFile], [mockRule]);
|
|
68
68
|
|
|
69
69
|
expect(diagnostics).toHaveLength(0);
|
|
70
70
|
});
|
|
@@ -115,7 +115,7 @@ const x = 1;`
|
|
|
115
115
|
},
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
const diagnostics = await runLint([testFile], [mockRule1, mockRule2]);
|
|
118
|
+
const { diagnostics } = await runLint([testFile], [mockRule1, mockRule2]);
|
|
119
119
|
|
|
120
120
|
expect(diagnostics).toHaveLength(1);
|
|
121
121
|
expect(diagnostics[0].ruleId).toBe("test-rule-2");
|
|
@@ -157,7 +157,7 @@ const y = 2;`
|
|
|
157
157
|
},
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
-
const diagnostics = await runLint([testFile], [mockRule]);
|
|
160
|
+
const { diagnostics } = await runLint([testFile], [mockRule]);
|
|
161
161
|
|
|
162
162
|
expect(diagnostics).toHaveLength(1);
|
|
163
163
|
expect(diagnostics[0].line).toBe(2);
|
|
@@ -200,7 +200,7 @@ const y = 2;`
|
|
|
200
200
|
},
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
-
const diagnostics = await runLint([testFile], [mockRule]);
|
|
203
|
+
const { diagnostics } = await runLint([testFile], [mockRule]);
|
|
204
204
|
|
|
205
205
|
expect(diagnostics).toHaveLength(1);
|
|
206
206
|
expect(diagnostics[0].line).toBe(3);
|
|
@@ -248,7 +248,7 @@ const y = 2;`
|
|
|
248
248
|
},
|
|
249
249
|
};
|
|
250
250
|
|
|
251
|
-
const diagnostics = await runLint([testFile], [mockRule1, mockRule2]);
|
|
251
|
+
const { diagnostics } = await runLint([testFile], [mockRule1, mockRule2]);
|
|
252
252
|
|
|
253
253
|
expect(diagnostics).toHaveLength(1);
|
|
254
254
|
expect(diagnostics[0].ruleId).toBe("test-rule-2");
|
|
@@ -282,7 +282,7 @@ const x = 1;`
|
|
|
282
282
|
},
|
|
283
283
|
};
|
|
284
284
|
|
|
285
|
-
const diagnostics = await runLint([testFile], [mockRule]);
|
|
285
|
+
const { diagnostics } = await runLint([testFile], [mockRule]);
|
|
286
286
|
|
|
287
287
|
expect(diagnostics).toHaveLength(1);
|
|
288
288
|
expect(diagnostics[0].ruleId).toBe("test-rule");
|
|
@@ -335,7 +335,7 @@ const z = 3;`
|
|
|
335
335
|
},
|
|
336
336
|
};
|
|
337
337
|
|
|
338
|
-
const diagnostics = await runLint([testFile], [mockRule]);
|
|
338
|
+
const { diagnostics } = await runLint([testFile], [mockRule]);
|
|
339
339
|
|
|
340
340
|
expect(diagnostics).toHaveLength(1);
|
|
341
341
|
expect(diagnostics[0].line).toBe(5);
|
|
@@ -367,7 +367,7 @@ const z = 3;`
|
|
|
367
367
|
},
|
|
368
368
|
};
|
|
369
369
|
|
|
370
|
-
const diagnostics = await runLint([testFile1, testFile2], [mockRule]);
|
|
370
|
+
const { diagnostics } = await runLint([testFile1, testFile2], [mockRule]);
|
|
371
371
|
|
|
372
372
|
expect(diagnostics).toHaveLength(2);
|
|
373
373
|
expect(diagnostics.map((d) => d.file).sort()).toEqual([testFile1, testFile2].sort());
|
|
@@ -388,7 +388,7 @@ const z = 3;`
|
|
|
388
388
|
},
|
|
389
389
|
};
|
|
390
390
|
|
|
391
|
-
const diagnostics = await runLint([testFile], [mockRule]);
|
|
391
|
+
const { diagnostics } = await runLint([testFile], [mockRule]);
|
|
392
392
|
expect(diagnostics).toHaveLength(0);
|
|
393
393
|
});
|
|
394
394
|
});
|
|
@@ -456,10 +456,286 @@ const x = 1;`
|
|
|
456
456
|
},
|
|
457
457
|
};
|
|
458
458
|
|
|
459
|
-
const diagnostics = await runLint([testFile], [mockRule1, mockRule2, mockRule3]);
|
|
459
|
+
const { diagnostics } = await runLint([testFile], [mockRule1, mockRule2, mockRule3]);
|
|
460
460
|
|
|
461
461
|
expect(diagnostics).toHaveLength(1);
|
|
462
462
|
expect(diagnostics[0].ruleId).toBe("test-rule-3");
|
|
463
463
|
});
|
|
464
464
|
});
|
|
465
|
+
|
|
466
|
+
test("parses reason from disable comment with -- separator", async () => {
|
|
467
|
+
await withTestDir(async (testDir) => {
|
|
468
|
+
const testFile = join(testDir, "test.ts");
|
|
469
|
+
await writeFile(
|
|
470
|
+
testFile,
|
|
471
|
+
`// chant-disable test-rule -- intentional for backwards compat
|
|
472
|
+
const x = 1;`
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const mockRule: LintRule = {
|
|
476
|
+
id: "test-rule",
|
|
477
|
+
severity: "error",
|
|
478
|
+
category: "correctness",
|
|
479
|
+
check: (context: LintContext): LintDiagnostic[] => {
|
|
480
|
+
return [
|
|
481
|
+
{
|
|
482
|
+
file: context.filePath,
|
|
483
|
+
line: 2,
|
|
484
|
+
column: 1,
|
|
485
|
+
ruleId: "test-rule",
|
|
486
|
+
severity: "error",
|
|
487
|
+
message: "Test error",
|
|
488
|
+
},
|
|
489
|
+
];
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const { diagnostics, suppressed } = await runLint([testFile], [mockRule]);
|
|
494
|
+
|
|
495
|
+
expect(diagnostics).toHaveLength(0);
|
|
496
|
+
expect(suppressed).toHaveLength(1);
|
|
497
|
+
expect(suppressed[0].ruleId).toBe("test-rule");
|
|
498
|
+
expect(suppressed[0].reason).toBe("intentional for backwards compat");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("parses reason from disable-all comment with -- separator", async () => {
|
|
503
|
+
await withTestDir(async (testDir) => {
|
|
504
|
+
const testFile = join(testDir, "test.ts");
|
|
505
|
+
await writeFile(
|
|
506
|
+
testFile,
|
|
507
|
+
`// chant-disable -- entire file suppressed for migration
|
|
508
|
+
const x = 1;`
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const mockRule: LintRule = {
|
|
512
|
+
id: "test-rule",
|
|
513
|
+
severity: "error",
|
|
514
|
+
category: "correctness",
|
|
515
|
+
check: (context: LintContext): LintDiagnostic[] => {
|
|
516
|
+
return [
|
|
517
|
+
{
|
|
518
|
+
file: context.filePath,
|
|
519
|
+
line: 2,
|
|
520
|
+
column: 1,
|
|
521
|
+
ruleId: "test-rule",
|
|
522
|
+
severity: "error",
|
|
523
|
+
message: "Test error",
|
|
524
|
+
},
|
|
525
|
+
];
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const { diagnostics, suppressed } = await runLint([testFile], [mockRule]);
|
|
530
|
+
|
|
531
|
+
expect(diagnostics).toHaveLength(0);
|
|
532
|
+
expect(suppressed).toHaveLength(1);
|
|
533
|
+
expect(suppressed[0].reason).toBe("entire file suppressed for migration");
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("backwards compat: no reason when -- is absent", async () => {
|
|
538
|
+
await withTestDir(async (testDir) => {
|
|
539
|
+
const testFile = join(testDir, "test.ts");
|
|
540
|
+
await writeFile(
|
|
541
|
+
testFile,
|
|
542
|
+
`// chant-disable test-rule
|
|
543
|
+
const x = 1;`
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const mockRule: LintRule = {
|
|
547
|
+
id: "test-rule",
|
|
548
|
+
severity: "error",
|
|
549
|
+
category: "correctness",
|
|
550
|
+
check: (context: LintContext): LintDiagnostic[] => {
|
|
551
|
+
return [
|
|
552
|
+
{
|
|
553
|
+
file: context.filePath,
|
|
554
|
+
line: 2,
|
|
555
|
+
column: 1,
|
|
556
|
+
ruleId: "test-rule",
|
|
557
|
+
severity: "error",
|
|
558
|
+
message: "Test error",
|
|
559
|
+
},
|
|
560
|
+
];
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const { diagnostics, suppressed } = await runLint([testFile], [mockRule]);
|
|
565
|
+
|
|
566
|
+
expect(diagnostics).toHaveLength(0);
|
|
567
|
+
expect(suppressed).toHaveLength(1);
|
|
568
|
+
expect(suppressed[0].reason).toBeUndefined();
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("handles reason with multiple -- in text", async () => {
|
|
573
|
+
await withTestDir(async (testDir) => {
|
|
574
|
+
const testFile = join(testDir, "test.ts");
|
|
575
|
+
await writeFile(
|
|
576
|
+
testFile,
|
|
577
|
+
`// chant-disable test-rule -- reason with -- dashes in it
|
|
578
|
+
const x = 1;`
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const mockRule: LintRule = {
|
|
582
|
+
id: "test-rule",
|
|
583
|
+
severity: "error",
|
|
584
|
+
category: "correctness",
|
|
585
|
+
check: (context: LintContext): LintDiagnostic[] => {
|
|
586
|
+
return [
|
|
587
|
+
{
|
|
588
|
+
file: context.filePath,
|
|
589
|
+
line: 2,
|
|
590
|
+
column: 1,
|
|
591
|
+
ruleId: "test-rule",
|
|
592
|
+
severity: "error",
|
|
593
|
+
message: "Test error",
|
|
594
|
+
},
|
|
595
|
+
];
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const { suppressed } = await runLint([testFile], [mockRule]);
|
|
600
|
+
|
|
601
|
+
expect(suppressed).toHaveLength(1);
|
|
602
|
+
expect(suppressed[0].reason).toBe("reason with -- dashes in it");
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("trailing -- with no text treats reason as undefined", async () => {
|
|
607
|
+
await withTestDir(async (testDir) => {
|
|
608
|
+
const testFile = join(testDir, "test.ts");
|
|
609
|
+
await writeFile(
|
|
610
|
+
testFile,
|
|
611
|
+
`// chant-disable test-rule --
|
|
612
|
+
const x = 1;`
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const mockRule: LintRule = {
|
|
616
|
+
id: "test-rule",
|
|
617
|
+
severity: "error",
|
|
618
|
+
category: "correctness",
|
|
619
|
+
check: (context: LintContext): LintDiagnostic[] => {
|
|
620
|
+
return [
|
|
621
|
+
{
|
|
622
|
+
file: context.filePath,
|
|
623
|
+
line: 2,
|
|
624
|
+
column: 1,
|
|
625
|
+
ruleId: "test-rule",
|
|
626
|
+
severity: "error",
|
|
627
|
+
message: "Test error",
|
|
628
|
+
},
|
|
629
|
+
];
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const { suppressed } = await runLint([testFile], [mockRule]);
|
|
634
|
+
|
|
635
|
+
expect(suppressed).toHaveLength(1);
|
|
636
|
+
expect(suppressed[0].reason).toBeUndefined();
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("disable-line with reason", async () => {
|
|
641
|
+
await withTestDir(async (testDir) => {
|
|
642
|
+
const testFile = join(testDir, "test.ts");
|
|
643
|
+
await writeFile(testFile, `const x = 1; // chant-disable-line test-rule -- CDN must use us-east-1`);
|
|
644
|
+
|
|
645
|
+
const mockRule: LintRule = {
|
|
646
|
+
id: "test-rule",
|
|
647
|
+
severity: "error",
|
|
648
|
+
category: "correctness",
|
|
649
|
+
check: (context: LintContext): LintDiagnostic[] => {
|
|
650
|
+
return [
|
|
651
|
+
{
|
|
652
|
+
file: context.filePath,
|
|
653
|
+
line: 1,
|
|
654
|
+
column: 1,
|
|
655
|
+
ruleId: "test-rule",
|
|
656
|
+
severity: "error",
|
|
657
|
+
message: "Test error",
|
|
658
|
+
},
|
|
659
|
+
];
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const { suppressed } = await runLint([testFile], [mockRule]);
|
|
664
|
+
|
|
665
|
+
expect(suppressed).toHaveLength(1);
|
|
666
|
+
expect(suppressed[0].reason).toBe("CDN must use us-east-1");
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("disable-next-line with reason", async () => {
|
|
671
|
+
await withTestDir(async (testDir) => {
|
|
672
|
+
const testFile = join(testDir, "test.ts");
|
|
673
|
+
await writeFile(
|
|
674
|
+
testFile,
|
|
675
|
+
`// chant-disable-next-line -- legacy code, will refactor
|
|
676
|
+
const x = 1;`
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
const mockRule: LintRule = {
|
|
680
|
+
id: "test-rule",
|
|
681
|
+
severity: "error",
|
|
682
|
+
category: "correctness",
|
|
683
|
+
check: (context: LintContext): LintDiagnostic[] => {
|
|
684
|
+
return [
|
|
685
|
+
{
|
|
686
|
+
file: context.filePath,
|
|
687
|
+
line: 2,
|
|
688
|
+
column: 1,
|
|
689
|
+
ruleId: "test-rule",
|
|
690
|
+
severity: "error",
|
|
691
|
+
message: "Test error",
|
|
692
|
+
},
|
|
693
|
+
];
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
const { suppressed } = await runLint([testFile], [mockRule]);
|
|
698
|
+
|
|
699
|
+
expect(suppressed).toHaveLength(1);
|
|
700
|
+
expect(suppressed[0].reason).toBe("legacy code, will refactor");
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("suppressed diagnostics contain the original diagnostic data", async () => {
|
|
705
|
+
await withTestDir(async (testDir) => {
|
|
706
|
+
const testFile = join(testDir, "test.ts");
|
|
707
|
+
await writeFile(
|
|
708
|
+
testFile,
|
|
709
|
+
`// chant-disable test-rule -- reason
|
|
710
|
+
const x = 1;`
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
const mockRule: LintRule = {
|
|
714
|
+
id: "test-rule",
|
|
715
|
+
severity: "error",
|
|
716
|
+
category: "correctness",
|
|
717
|
+
check: (context: LintContext): LintDiagnostic[] => {
|
|
718
|
+
return [
|
|
719
|
+
{
|
|
720
|
+
file: context.filePath,
|
|
721
|
+
line: 2,
|
|
722
|
+
column: 1,
|
|
723
|
+
ruleId: "test-rule",
|
|
724
|
+
severity: "error",
|
|
725
|
+
message: "Test error",
|
|
726
|
+
},
|
|
727
|
+
];
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const { suppressed } = await runLint([testFile], [mockRule]);
|
|
732
|
+
|
|
733
|
+
expect(suppressed[0].file).toBe(testFile);
|
|
734
|
+
expect(suppressed[0].line).toBe(2);
|
|
735
|
+
expect(suppressed[0].column).toBe(1);
|
|
736
|
+
expect(suppressed[0].ruleId).toBe("test-rule");
|
|
737
|
+
expect(suppressed[0].severity).toBe("error");
|
|
738
|
+
expect(suppressed[0].message).toBe("Test error");
|
|
739
|
+
});
|
|
740
|
+
});
|
|
465
741
|
});
|
package/src/lint/engine.ts
CHANGED
|
@@ -2,6 +2,16 @@ import type { LintRule, LintDiagnostic, LintContext } from "./rule";
|
|
|
2
2
|
import { parseFile } from "./parser";
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Result of a lint run, separating active diagnostics from suppressed ones.
|
|
7
|
+
*/
|
|
8
|
+
export interface LintRunResult {
|
|
9
|
+
/** Diagnostics that were not suppressed by disable directives */
|
|
10
|
+
diagnostics: LintDiagnostic[];
|
|
11
|
+
/** Diagnostics that were suppressed by disable directives, with optional reason */
|
|
12
|
+
suppressed: Array<LintDiagnostic & { reason?: string }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
/**
|
|
6
16
|
* Represents a disable directive found in source code comments
|
|
7
17
|
*/
|
|
@@ -12,6 +22,8 @@ interface DisableDirective {
|
|
|
12
22
|
type: "file" | "line" | "next-line";
|
|
13
23
|
/** Specific rule IDs to disable, or undefined for all rules */
|
|
14
24
|
ruleIds?: string[];
|
|
25
|
+
/** Optional reason for the suppression (text after ` -- `) */
|
|
26
|
+
reason?: string;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
/**
|
|
@@ -24,6 +36,60 @@ interface DisableDirective {
|
|
|
24
36
|
* - // chant-disable-line rule-id1 - disable specific rules for line
|
|
25
37
|
* - // chant-disable-next-line rule-id1 - disable specific rules for next line
|
|
26
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* Parse the text after a directive keyword, splitting rule IDs from an optional reason.
|
|
41
|
+
* Reason is separated by ` -- ` (first occurrence only).
|
|
42
|
+
* Trailing `--` with no text is treated as no reason.
|
|
43
|
+
*/
|
|
44
|
+
function parseDirectiveText(afterDirective: string | undefined): {
|
|
45
|
+
ruleIds?: string[];
|
|
46
|
+
reason?: string;
|
|
47
|
+
} {
|
|
48
|
+
if (!afterDirective) return {};
|
|
49
|
+
|
|
50
|
+
const trimmed = afterDirective.trim();
|
|
51
|
+
if (!trimmed) return {};
|
|
52
|
+
|
|
53
|
+
// Handle `-- reason` at the start (no rule IDs, just reason)
|
|
54
|
+
if (trimmed.startsWith("-- ")) {
|
|
55
|
+
const reasonPart = trimmed.slice(3).trim();
|
|
56
|
+
return { reason: reasonPart || undefined };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Handle bare `--` (no rule IDs, no reason)
|
|
60
|
+
if (trimmed === "--") {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle `RULE_ID -- reason` (rule IDs + reason separated by ` -- `)
|
|
65
|
+
const dashIdx = trimmed.indexOf(" -- ");
|
|
66
|
+
if (dashIdx >= 0) {
|
|
67
|
+
const idsPart = trimmed.slice(0, dashIdx).trim();
|
|
68
|
+
const reasonPart = trimmed.slice(dashIdx + 4).trim();
|
|
69
|
+
const ruleIds = idsPart ? idsPart.split(/\s+/).filter(Boolean) : undefined;
|
|
70
|
+
const reason = reasonPart || undefined;
|
|
71
|
+
return {
|
|
72
|
+
ruleIds: ruleIds && ruleIds.length > 0 ? ruleIds : undefined,
|
|
73
|
+
reason,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle trailing ` --` with no reason text
|
|
78
|
+
if (trimmed.endsWith(" --")) {
|
|
79
|
+
const idsPart = trimmed.slice(0, -3).trim();
|
|
80
|
+
const ruleIds = idsPart ? idsPart.split(/\s+/).filter(Boolean) : undefined;
|
|
81
|
+
return {
|
|
82
|
+
ruleIds: ruleIds && ruleIds.length > 0 ? ruleIds : undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// No `--` at all — just rule IDs
|
|
87
|
+
const ruleIds = trimmed.split(/\s+/).filter(Boolean);
|
|
88
|
+
return {
|
|
89
|
+
ruleIds: ruleIds.length > 0 ? ruleIds : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
27
93
|
function parseDisableComments(content: string): DisableDirective[] {
|
|
28
94
|
const directives: DisableDirective[] = [];
|
|
29
95
|
const lines = content.split("\n");
|
|
@@ -38,25 +104,28 @@ function parseDisableComments(content: string): DisableDirective[] {
|
|
|
38
104
|
const disableNextLineMatch = line.match(/\/\/\s*chant-disable-next-line(?:\s+(.+))?$/);
|
|
39
105
|
|
|
40
106
|
if (disableNextLineMatch) {
|
|
41
|
-
const ruleIds = disableNextLineMatch[1]
|
|
107
|
+
const { ruleIds, reason } = parseDirectiveText(disableNextLineMatch[1]);
|
|
42
108
|
directives.push({
|
|
43
109
|
line: lineNumber + 1, // Next line
|
|
44
110
|
type: "next-line",
|
|
45
|
-
ruleIds
|
|
111
|
+
ruleIds,
|
|
112
|
+
reason,
|
|
46
113
|
});
|
|
47
114
|
} else if (disableLineMatch) {
|
|
48
|
-
const ruleIds = disableLineMatch[1]
|
|
115
|
+
const { ruleIds, reason } = parseDirectiveText(disableLineMatch[1]);
|
|
49
116
|
directives.push({
|
|
50
117
|
line: lineNumber,
|
|
51
118
|
type: "line",
|
|
52
|
-
ruleIds
|
|
119
|
+
ruleIds,
|
|
120
|
+
reason,
|
|
53
121
|
});
|
|
54
122
|
} else if (disableFileMatch) {
|
|
55
|
-
const ruleIds = disableFileMatch[1]
|
|
123
|
+
const { ruleIds, reason } = parseDirectiveText(disableFileMatch[1]);
|
|
56
124
|
directives.push({
|
|
57
125
|
line: lineNumber,
|
|
58
126
|
type: "file",
|
|
59
|
-
ruleIds
|
|
127
|
+
ruleIds,
|
|
128
|
+
reason,
|
|
60
129
|
});
|
|
61
130
|
}
|
|
62
131
|
}
|
|
@@ -65,24 +134,25 @@ function parseDisableComments(content: string): DisableDirective[] {
|
|
|
65
134
|
}
|
|
66
135
|
|
|
67
136
|
/**
|
|
68
|
-
* Check if a diagnostic should be suppressed based on disable directives
|
|
137
|
+
* Check if a diagnostic should be suppressed based on disable directives.
|
|
138
|
+
* Returns suppression info if suppressed, or null if not suppressed.
|
|
69
139
|
*/
|
|
70
140
|
function isDiagnosticDisabled(
|
|
71
141
|
diagnostic: LintDiagnostic,
|
|
72
142
|
directives: DisableDirective[],
|
|
73
143
|
allRuleIds: Set<string>
|
|
74
|
-
): boolean {
|
|
144
|
+
): { suppressed: boolean; reason?: string } {
|
|
75
145
|
// Check for file-level disables
|
|
76
146
|
const fileDisables = directives.filter((d) => d.type === "file");
|
|
77
147
|
for (const directive of fileDisables) {
|
|
78
148
|
if (!directive.ruleIds) {
|
|
79
149
|
// Disable all rules
|
|
80
|
-
return true;
|
|
150
|
+
return { suppressed: true, reason: directive.reason };
|
|
81
151
|
}
|
|
82
152
|
// Check if rule ID exists before checking if it's disabled
|
|
83
153
|
if (directive.ruleIds.some((id) => allRuleIds.has(id))) {
|
|
84
154
|
if (directive.ruleIds.includes(diagnostic.ruleId)) {
|
|
85
|
-
return true;
|
|
155
|
+
return { suppressed: true, reason: directive.reason };
|
|
86
156
|
}
|
|
87
157
|
}
|
|
88
158
|
// Silently ignore non-existent rule IDs
|
|
@@ -95,18 +165,18 @@ function isDiagnosticDisabled(
|
|
|
95
165
|
for (const directive of lineDisables) {
|
|
96
166
|
if (!directive.ruleIds) {
|
|
97
167
|
// Disable all rules
|
|
98
|
-
return true;
|
|
168
|
+
return { suppressed: true, reason: directive.reason };
|
|
99
169
|
}
|
|
100
170
|
// Check if rule ID exists before checking if it's disabled
|
|
101
171
|
if (directive.ruleIds.some((id) => allRuleIds.has(id))) {
|
|
102
172
|
if (directive.ruleIds.includes(diagnostic.ruleId)) {
|
|
103
|
-
return true;
|
|
173
|
+
return { suppressed: true, reason: directive.reason };
|
|
104
174
|
}
|
|
105
175
|
}
|
|
106
176
|
// Silently ignore non-existent rule IDs
|
|
107
177
|
}
|
|
108
178
|
|
|
109
|
-
return false;
|
|
179
|
+
return { suppressed: false };
|
|
110
180
|
}
|
|
111
181
|
|
|
112
182
|
/**
|
|
@@ -114,14 +184,15 @@ function isDiagnosticDisabled(
|
|
|
114
184
|
* @param files - Array of file paths to lint
|
|
115
185
|
* @param rules - Array of lint rules to execute
|
|
116
186
|
* @param ruleOptions - Optional map of rule ID to options object
|
|
117
|
-
* @returns
|
|
187
|
+
* @returns LintRunResult with diagnostics and suppressed items
|
|
118
188
|
*/
|
|
119
189
|
export async function runLint(
|
|
120
190
|
files: string[],
|
|
121
191
|
rules: LintRule[],
|
|
122
192
|
ruleOptions?: Map<string, Record<string, unknown>>,
|
|
123
|
-
): Promise<
|
|
193
|
+
): Promise<LintRunResult> {
|
|
124
194
|
const allDiagnostics: LintDiagnostic[] = [];
|
|
195
|
+
const allSuppressed: Array<LintDiagnostic & { reason?: string }> = [];
|
|
125
196
|
const allRuleIds = new Set(rules.map((r) => r.id));
|
|
126
197
|
|
|
127
198
|
for (const filePath of files) {
|
|
@@ -148,21 +219,28 @@ export async function runLint(
|
|
|
148
219
|
allDiagnostics.push(...diagnostics);
|
|
149
220
|
}
|
|
150
221
|
|
|
151
|
-
//
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
222
|
+
// Partition file diagnostics into active and suppressed
|
|
223
|
+
const fileDiags = allDiagnostics.filter((d) => d.file === filePath);
|
|
224
|
+
const otherFileDiags = allDiagnostics.filter((d) => d.file !== filePath);
|
|
225
|
+
|
|
226
|
+
const keptDiags: LintDiagnostic[] = [];
|
|
227
|
+
for (const diagnostic of fileDiags) {
|
|
228
|
+
const result = isDiagnosticDisabled(diagnostic, directives, allRuleIds);
|
|
229
|
+
if (result.suppressed) {
|
|
230
|
+
allSuppressed.push({ ...diagnostic, reason: result.reason });
|
|
231
|
+
} else {
|
|
232
|
+
keptDiags.push(diagnostic);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
155
235
|
|
|
156
236
|
// Replace file diagnostics with filtered ones
|
|
157
|
-
const otherFileDiagnostics = allDiagnostics.filter((d) => d.file !== filePath);
|
|
158
237
|
allDiagnostics.length = 0;
|
|
159
|
-
allDiagnostics.push(...
|
|
238
|
+
allDiagnostics.push(...otherFileDiags, ...keptDiags);
|
|
160
239
|
} catch (error) {
|
|
161
240
|
// If parsing fails, skip this file and continue with others
|
|
162
|
-
// In a real implementation, you might want to collect these errors
|
|
163
241
|
continue;
|
|
164
242
|
}
|
|
165
243
|
}
|
|
166
244
|
|
|
167
|
-
return allDiagnostics;
|
|
245
|
+
return { diagnostics: allDiagnostics, suppressed: allSuppressed };
|
|
168
246
|
}
|