Skip to content

Write Lint Rules

Lexicons can contribute three kinds of validation: imperative rules (TypeScript compiler API), declarative rules (pattern matching DSL), and post-synth checks (validate serialized output).

Rules target a resource kind by name, so a rule works the same whether the kind is a built-in resource or one generated from an imported CRD. The k8s lexicon’s ARGO001ARGO005 are the worked example: they lint the Argo Application kind, which arrives through a CRD source, not hand-written types.

Imperative rules use the TypeScript compiler API to inspect AST nodes:

import type { LintRule, LintContext, LintDiagnostic } from "@intentius/chant";
export const myRule: LintRule = {
id: "MYD001",
severity: "warning",
category: "style",
description: "Example custom rule",
check(context: LintContext): LintDiagnostic[] {
// Your rule logic here
return [];
},
};

Wire imperative and declarative rules by importing them into the lintRules() array in your plugin.ts — they are not auto-discovered (post-synth checks are; see Post-Synth Checks).

plugin.ts
import { argoAutomatedPruneRule } from "./lint/rules/argo-automated-prune";
lintRules(): LintRule[] {
return [argoAutomatedPruneRule /* , ...others */];
}

Declarative rules use the rule() builder for common patterns:

import type { RuleSpec } from "@intentius/chant";
const spec: RuleSpec = {
id: "MYD002",
severity: "warning",
category: "style",
description: "Resource names must be lowercase",
selector: "resource > property",
match: { pattern: /^[A-Z]/ },
message: "Property name '{node}' should be lowercase",
};

Return these from your plugin’s declarativeRules() method.

Post-synth checks validate the serialized output after the build pipeline completes:

import type { PostSynthCheck, PostSynthDiagnostic } from "@intentius/chant";
const myCheck: PostSynthCheck = {
id: "MYD010",
description: "Check output constraints",
check(context): PostSynthDiagnostic[] {
// Validate the serialized output
return [];
},
};

Return these from your plugin’s postSynthChecks() method.

Rule IDs follow the pattern {PREFIX}{CATEGORY}{NUMBER}:

  • Prefix — your lexicon’s rulePrefix (e.g. MY, AWS, K8S)
  • Category — single letter: D (declarative), S (style), C (correctness), E (evaluability)
  • Number — three-digit sequential number

Every post-synth check needs both positive and negative tests. Use a makeCtx helper:

function makeCtx(yaml: string) {
return {
outputs: new Map([["my-lexicon", yaml]]),
};
}
describe("MYD010: description", () => {
test("flags bad pattern", () => {
const yaml = `...bad yaml...`;
const diags = myCheck.check(makeCtx(yaml));
expect(diags.length).toBeGreaterThanOrEqual(1);
expect(diags[0].checkId).toBe("MYD010");
});
test("no diagnostic when correct", () => {
const yaml = `...good yaml...`;
const diags = myCheck.check(makeCtx(yaml));
expect(diags).toHaveLength(0);
});
});

Test imperative rules by constructing a minimal AST context and verifying the returned diagnostics array. Test declarative rules by running the rule() builder output against sample input strings.

See Testing Your Lexicon for the full testing guide, including patterns for all test file types.

With rules in place, add LSP & MCP providers for editor integration.