Gå til innholdet

Skrive regler

Regler er TypeScript-funksjoner som sjekker kodebasen din for ADR-samsvar. De ligger i tilhørende .rules.ts-filer ved siden av ADR-markdown-filer og kjøres når du utfører archgate check.

.archgate/adrs/
ARCH-001-command-structure.md # Beslutningen
ARCH-001-command-structure.rules.ts # De automatiserte sjekkene

Hver regelfil eksporterer et standard-objekt typet med satisfies RuleSet. Hver nøkkel i rules-objektet blir en regel-ID, og hver regel har en description og en asynkron check-funksjon som mottar et kontekstobjekt.

/// <reference path="../rules.d.ts" />
export default {
rules: {
"my-rule-id": {
description: "What this rule checks",
async check(ctx) {
// Your check logic here
},
},
},
} satisfies RuleSet;

En enkelt regelfil kan definere flere regler:

/// <reference path="../rules.d.ts" />
export default {
rules: {
"first-rule": {
description: "Checks one thing",
async check(ctx) {
// ...
},
},
"second-rule": {
description: "Checks another thing",
async check(ctx) {
// ...
},
},
},
} satisfies RuleSet;

ctx-objektet som sendes til hver check-funksjon gir muligheter for fillesing, søk og rapportering. Her er en detaljert referanse med eksempler.

En matrise med filstier som matcher ADR-ens files-glob fra frontmatter. Hvis ADR-en ikke har noe files-felt, inkluderer dette alle prosjektfiler.

for (const file of ctx.scopedFiles) {
const content = await ctx.readFile(file);
// Check content...
}

Bruk ctx.scopedFiles når regelen din bare skal gjelde filer ADR-en styrer. For eksempel vil en kommandostrukturregel avgrenset til src/commands/**/*.ts bare motta kommandofiler.

En matrise med filstier som skiller seg fra grunngrenen, inkludert ikke-committede endringer i arbeidstreet (stagede, ustagede og usporede ikke-ignorerte filer). Auto-detektert som standard, eller fylt fra --staged / --base <ref>. Nyttig for inkrementell sjekking og regler for avhengigheter mellom filer.

// Incremental checking -- only validate changed files
const filesToCheck = ctx.scopedFiles.filter((f) =>
ctx.changedFiles.includes(f)
);
// Cross-file dependency -- if file A changed, file B must also change
if (ctx.changedFiles.includes("config/database.yml")) {
if (!ctx.changedFiles.includes("deploy/manifest.yml")) {
ctx.report.violation({
message: "config changed but manifest was not bumped",
file: "config/database.yml",
});
}
}

Les innholdet i en fil som en streng. Stien er relativ til prosjektroten.

const content = await ctx.readFile("src/cli.ts");

Les og parse en JSON-fil. Returnerer unknown — cast den til forventet form.

const pkg = (await ctx.readJSON("package.json")) as {
dependencies?: Record<string, string>;
};

Søk i en enkelt fil med et regulært uttrykk. Returnerer en matrise med GrepMatch-objekter, hver med egenskapene file, line, column og content.

const matches = await ctx.grep(file, /console\.error\(/);
for (const match of matches) {
ctx.report.violation({
message: "Use logError() instead of console.error()",
file: match.file,
line: match.line,
});
}

Søk på tvers av flere filer som matcher et glob-mønster. Returnerer en flat matrise med GrepMatch-objekter fra alle matchende filer. Filer ignorert av .gitignore ekskluderes som standard. Sett respectGitignore: false i ADR-frontmatter for å inkludere dem.

const matches = await ctx.grepFiles(/TODO:/, "src/**/*.ts");
for (const match of matches) {
ctx.report.warning({
message: "TODO comment found",
file: match.file,
line: match.line,
});
}

Finn filer etter glob-mønster. Returnerer en matrise med filstier relative til prosjektroten. Filer ignorert av .gitignore ekskluderes som standard. Sett respectGitignore: false i ADR-frontmatter for å inkludere dem.

const testFiles = await ctx.glob("tests/**/*.test.ts");

Parse en kildefil til dens språknative AST. Støttede språk: "typescript", "javascript", "python" og "ruby". TypeScript og JavaScript parses i prosessen til en ESTree Program; Python og Ruby starter systemtolkens parser fra standardbiblioteket som en underprosess. Kaster ved parsefeil eller manglende tolk — den returnerer aldri null.

const program = await ctx.ast("src/cli.ts", "typescript");

Se Strukturelle sjekker med ctx.ast() nedenfor for komplette eksempler, og Regel-API-referansen for den returnerte formen per språk.

Rapporteringsgrensesnittet med tre alvorlighetsgradsmetoder:

  • ctx.report.violation(detail) — feil-alvorlighetsgrad (exit-kode 1, blokkerer CI)
  • ctx.report.warning(detail) — advarsel-alvorlighetsgrad (logges, men blokkerer ikke)
  • ctx.report.info(detail) — informasjon (logges for synlighet)

Hver metode aksepterer et objekt med:

FeltTypePåkrevdBeskrivelse
messagestringJaHva bruddet er
filestringNeiSti til den problematiske filen
linenumberNeiLinjenummer for bruddet
fixstringNeiForeslått løsning (vises til utvikleren)
ctx.report.violation({
message: "Command file must export a register*Command function",
file: "src/commands/check.ts",
line: 5,
fix: "Add: export function registerCheckCommand(program: Command) { ... }",
});

Den absolutte stien til prosjektets rotkatalog. Nyttig når du trenger å konstruere absolutte stier.

Regulære uttrykk fungerer for overflatemønstre, men kommer til kort for strukturelle spørsmål som “inneholder denne filen bare re-eksporter?” eller “er dette en naken except:-klausul?” — flerlinjesetninger, kommentarer og strenginnhold slår alle beina under linjebasert matching. ctx.ast() parser en fil til et ekte syntakstre slik at regelen din kan sjekke strukturen direkte.

Det returnerte treet er språknativt, ikke enhetlig på tvers av språk:

  • TypeScript / JavaScript — en ESTree Program parset i prosessen av meriyah. TypeScript transpileres først, så typenivå-syntaks (interface, typealiaser, export type { ... } from) fjernes fra treet; en fil som bare inneholder setninger på typenivå parses til en tom Program-body.
  • Python — treet fra standardbibliotekets ast-modul serialisert til JSON. Hver node er et objekt med et _type-felt pluss nodens egne felt og posisjonene lineno / col_offset.
  • RubyRipper.sexp-utdata fra standardbibliotekets Ripper: nestede matriser som ["program", [["command", ...]]] med [line, column]-posisjonspar.

ctx.ast() kaster ved parsefeil eller manglende tolk — den returnerer aldri null. Kastet er isolert til regelen som feiler og vises som en regelkjøringsfeil (exit-kode 2), slik at et ødelagt miljø dukker opp som en synlig feil i stedet for en falsk godkjenning.

En barrel-fil er en index.ts der hver setning på toppnivå er en re-eksport. Med ESTree-Program-et blir det spørsmålet en direkte sjekk av setningstyper i stedet for en linjematchende heuristikk:

/// <reference path="../rules.d.ts" />
export default {
rules: {
"no-barrel-files": {
description: "index.ts files must not be pure re-export barrels",
async check(ctx) {
const indexFiles = ctx.scopedFiles.filter((f) =>
f.endsWith("/index.ts")
);
const checks = indexFiles.map(async (file) => {
const program = await ctx.ast(file, "typescript");
const isBarrel =
program.body.length > 0 &&
program.body.every(
(node) =>
node.type === "ExportAllDeclaration" ||
(node.type === "ExportNamedDeclaration" && node.source !== null)
);
if (isBarrel) {
ctx.report.violation({
message: `Barrel file detected: ${file} contains only re-exports`,
file,
fix: "Delete the barrel and import directly from the source modules",
});
}
});
await Promise.all(checks);
},
},
},
} satisfies RuleSet;

Fordi TypeScript transpileres før parsing, fjernes export type { ... } from-re-eksporter — en barrel som bare inneholder type-re-eksporter parses til en tom body, som body.length > 0-vakten hopper over. Hvis du også trenger å flagge rene type-barrels, kombiner AST-sjekken med en tekstsjekk. Den samme transpileringen forskyver også posisjoner: loc-linjenumre refererer til den transpilerte teksten, ikke den opprinnelige .ts-filen, så en "typescript"-regel må finne konstruksjonen på nytt i den opprinnelige kildekoden (for eksempel med ctx.readFile() og indexOf) før den rapporterer en line — eller utelate line.

Gå gjennom ESTree-treet etter CallExpression-noder der callee er identifikatoren require. En rekursiv gjennomgang av objektverdier og matriser dekker alle nodetyper uten å måtte liste dem opp:

/// <reference path="../rules.d.ts" />
function findRequireCalls(node: unknown, lines: number[]): void {
if (Array.isArray(node)) {
for (const item of node) findRequireCalls(item, lines);
return;
}
if (node === null || typeof node !== "object") return;
const n = node as EsTreeNode;
const callee = n.callee as EsTreeNode | undefined;
if (
n.type === "CallExpression" &&
callee?.type === "Identifier" &&
callee.name === "require"
) {
if (n.loc) lines.push(n.loc.start.line);
}
for (const value of Object.values(n)) findRequireCalls(value, lines);
}
export default {
rules: {
"no-require-in-esm": {
description: ".mjs files must not call CommonJS require()",
async check(ctx) {
const files = await ctx.glob("src/**/*.mjs");
const checks = files.map(async (file) => {
const program = await ctx.ast(file, "javascript");
const lines: number[] = [];
findRequireCalls(program, lines);
for (const line of lines) {
ctx.report.violation({
message: "CommonJS require() call in an ES module",
file,
line,
fix: "Use a static import or await import() instead",
});
}
});
await Promise.all(checks);
},
},
},
} satisfies RuleSet;

loc.start.line-rapporteringen her er gyldig bare fordi "javascript" parser den opprinnelige kildekoden utranspilert — en "typescript"-regel må i stedet finne linjen i den opprinnelige kildekoden, siden dens loc refererer til det transpilerte resultatet.

I Pythons ast-modul er en except:-klausul en ExceptHandler-node der type-feltet holder uttrykket for unntaket som fanges — en naken except: har "type": null. Gå gjennom JSON-treet etter den formen:

/// <reference path="../rules.d.ts" />
function findBareExcepts(node: unknown, lines: number[]): void {
if (Array.isArray(node)) {
for (const item of node) findBareExcepts(item, lines);
return;
}
if (node === null || typeof node !== "object") return;
const n = node as PythonAstNode;
if (n._type === "ExceptHandler" && n.type === null) {
if (n.lineno !== undefined) lines.push(n.lineno);
}
for (const value of Object.values(n)) findBareExcepts(value, lines);
}
export default {
rules: {
"no-bare-except": {
description: "Python code must not use bare except: clauses",
async check(ctx) {
const files = await ctx.glob("**/*.py");
const checks = files.map(async (file) => {
const tree = await ctx.ast(file, "python");
const lines: number[] = [];
findBareExcepts(tree, lines);
for (const line of lines) {
ctx.report.violation({
message:
"Bare except: catches every exception, including SystemExit",
file,
line,
fix: "Catch a specific exception class, e.g. except ValueError:",
});
}
});
await Promise.all(checks);
},
},
},
} satisfies RuleSet;

Ripper.sexp representerer puts "hello" som ["command", ["@ident", "puts", [1, 0]], [...args]][line, column]-paret ligger inne i @ident-tokenet. Gå gjennom de nestede matrisene etter den formen:

/// <reference path="../rules.d.ts" />
function findPutsCalls(node: unknown, lines: number[]): void {
if (!Array.isArray(node)) return;
const [kind, first] = node;
if (
kind === "command" &&
Array.isArray(first) &&
first[0] === "@ident" &&
first[1] === "puts"
) {
const [line] = first[2] as [number, number];
lines.push(line);
}
for (const child of node) findPutsCalls(child, lines);
}
export default {
rules: {
"no-puts": {
description: "Ruby code must use the application logger, not puts",
async check(ctx) {
const files = await ctx.glob("app/**/*.rb");
const checks = files.map(async (file) => {
const sexp = await ctx.ast(file, "ruby");
const lines: number[] = [];
findPutsCalls(sexp, lines);
for (const line of lines) {
ctx.report.violation({
message: "puts writes to stdout directly",
file,
line,
fix: "Replace puts with logger.info",
});
}
});
await Promise.all(checks);
},
},
},
} satisfies RuleSet;

Ripper bruker en annen nodeform for varianten med parenteser — puts("hello") vises under et method_add_arg / fcall-par i stedet for command — så en produksjonsregel ville matchet fcall-tokenet på samme måte.

Hver regel kan sette en standard alvorlighetsgrad i konfigurasjonen sin. Alvorlighetsgraden bestemmer hvordan brudd behandles:

AlvorlighetsgradExit-kodeOppførsel
error1Blokkerer CI, må fikses
warning0Logges, men blokkerer ikke
info0Informasjon, logges for synlighet

Sett alvorlighetsgraden i regeldefinisjonen:

export default {
rules: {
"my-rule": {
description: "...",
severity: "warning",
async check(ctx) {
// Violations from this rule are warnings, not errors
ctx.report.violation({ message: "..." });
},
},
},
} satisfies RuleSet;

Hvis severity utelates, er standardverdien error.

Du kan også rapportere med ulike alvorlighetsgrader innenfor samme regel ved å bruke ctx.report.violation(), ctx.report.warning() og ctx.report.info() direkte.

Hver regel har en kjøringstidsgrense på 30 sekunder. Hvis en regel overskrider denne grensen, behandles den som en feil. Dette forhindrer at uendelige sjekker blokkerer pipelinen.

Hold reglene raske ved å:

  • Bruke ctx.grepFiles() i stedet for å lese hver fil manuelt
  • Bruke Promise.all() for å sjekke filer parallelt
  • Avgrense regler med files-feltet i frontmatter for å begrense antall filer som behandles

fix-feltet er en valgfri streng som vises til utvikleren sammen med bruddmeldingen. Den beskriver hvilken handling som må utføres for å løse problemet. Fikser brukes ikke automatisk — de er veiledning.

ctx.report.violation({
message: `Unapproved dependency: "chalk"`,
file: "package.json",
fix: "Use styleText() from node:util instead of chalk",
});

Når den vises, kommer fiksen under bruddmeldingen:

ARCH-006/no-unapproved-deps
package.json
Unapproved dependency: "chalk"
Fix: Use styleText() from node:util instead of chalk
  1. Bruk Promise.all() for parallelle filsjekker. Når du sjekker flere filer uavhengig, prosesser dem parallelt i stedet for sekvensielt.

    // Good: parallel
    const checks = files.map(async (file) => {
    const content = await ctx.readFile(file);
    // ...
    });
    await Promise.all(checks);
    // Avoid: sequential
    for (const file of files) {
    const content = await ctx.readFile(file);
    // ...
    }
  2. Bruk ctx.changedFiles for inkrementell sjekking. ctx.changedFiles fylles automatisk med grenforskjellen pluss ikke-committede endringer i arbeidstreet (eller stagede filer med --staged). Filtrer ctx.scopedFiles mot den for å bare sjekke det som er endret, eller bruk den direkte for regler om avhengigheter mellom filer.

  3. Hold reglene fokusert på ett anliggende. En regel som sjekker både navnekonvensjoner og importmønstre bør deles i to regler med separate ID-er.

  4. Bruk ctx.grepFiles() fremfor manuell iterering. Når du søker etter et mønster på tvers av mange filer, er ctx.grepFiles() mer effektivt enn å lese hver fil og kjøre et regulært uttrykk.

  5. Gi handlingsbare fix-meldinger. En fiks som “Ikke gjør dette” er ikke nyttig. Fortell utvikleren nøyaktig hva som skal gjøres i stedet.

  6. Filtrer bort ikke-relevante filer tidlig. Hvis regelen din bare gjelder for bestemte filer innenfor omfanget, filtrer ctx.scopedFiles før prosessering:

    const commandFiles = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
  7. Håndter manglende filer elegant. Hvis regelen din leser en spesifikk fil som package.json, pakk lesingen i en try/catch og returner tidlig hvis filen ikke eksisterer.

Det finnes to måter å håndtere unntak i Archgate: undertrykkelse på motornivå (fungerer med alle regler automatisk) og egendefinerte direktiver på regelnivå (implementert av regelforfatteren for domenespesifikke opt-outs).

Archgate støtter inline archgate-ignore-kommentarer som undertrykker brudd uten å endre selve regelen. Motoren analyserer disse kommentarene og filtrerer matchende brudd før rapportering.

Neste-linje-undertrykkelse: undertrykker bruddet på den umiddelbart følgende linjen:

// archgate-ignore ARCH-006/no-unapproved-deps legacy-avhengighet, migrering planlagt for Q3
import chalk from "chalk";

Filnivå-undertrykkelse: undertrykker alle matchende brudd hvor som helst i filen:

// archgate-ignore-file ARCH-005/test-mirrors-src generert fil, ingen manuell test

Flere regler: stable kommentarer for å undertrykke mer enn én regel på samme linje:

// archgate-ignore ARCH-006/no-unapproved-deps legacy-avhengighet
// archgate-ignore ARCH-003/use-style-text tredjepartsbibliotek håndterer farger
import chalk from "chalk";

Sammenhengende undertrykkelseskommentarer sikter alle mot den første ikke-undertrykkelseslinjen etter blokken.

Formatet er ADR-ID/rule-id etterfulgt av en begrunnelse. Begrunnelsen er påkrevd. En undertrykkelse uten begrunnelse ignoreres og gir en advarsel:

[suppression] Suppression for ARCH-006/no-unapproved-deps is missing a reason src/foo.ts:1

Både // og # kommentarstiler støttes, slik at undertrykkelser fungerer i TypeScript, JavaScript, YAML, Python, shell-skript og andre filtyper reglene dine kan skanne.

For domenespesifikke opt-outs kan regelforfattere implementere sine egne kommentarbaserte direktiver inne i check-funksjonen. Dette mønsteret gir regelen full kontroll over direktivsyntaksen, plassering og validering.

// I din .rules.ts-fil:
async check(ctx) {
const files = await ctx.glob("src/components/**/*Connected.tsx");
for (const file of files) {
const content = await ctx.readFile(file);
// Støtt opt-out-direktiv øverst i filen
if (/^\/\/\s*@no-presentational:/u.test(content.trimStart())) continue;
// ... regellogikk som kan rapportere et brudd ...
ctx.report.violation({
message: "Manglende presentasjonskomponent",
file,
fix: 'Legg til "// @no-presentational: <begrunnelse>" øverst i filen for å gjøre opt-out',
});
}
}

Utvikleren gjør opt-out ved å legge til direktivet i filen sin:

// @no-presentational: denne komponenten bare omdirigerer, ingen UI å rendre
import { useNavigate } from "react-router";
TilnærmingBest forHvem kontrollerer
archgate-ignoreAd-hoc-unntak for alle reglerUtvikleren som bruker regelen
Egendefinert direktivDomenespesifikke opt-outs med strukturerte begrunnelserRegelforfatteren

Bruk archgate-ignore når en utvikler trenger å undertrykke et engangsbrudd. Bruk egendefinerte direktiver når opt-outen er et førsteklasses konsept i regelens domene, for eksempel å markere en komponent som med vilje uparet, eller en fil som automatisk generert.

  • Vanlige regelmønstre — Kopier-og-lim-inn-mønstre organisert etter kategori: avhengighetshåndtering, importrestriksjoner, filstruktur, kodekvalitet, databaseskjema og arkitekturgrenser.
  • Regel-API-referanse — Fullstendig referanse for alle regel-API-typer og -funksjoner.
  • CI-integrasjon — Koble archgate check til pipelinen din for å håndheve regler på hver PR.