Rule execution
Source: src/engine/rule-runner.ts
Runs every enabled rule against the project AST and collects diagnostics. Rules inspect the AST and the project structure, then report anti-patterns as diagnostics that say what is wrong and how to fix it.
See the rules overview for how to write and register a rule.
Input is the parsed project, the file list, the enabled rules, and the graphs they read:
project: Project // AST from the AST parsing stage
files: string[] // file paths
rules: AnyRule[] // enabled rules (after config filtering)
options: {
config: NestjsDoctorConfig
moduleGraph: ModuleGraph
providers: Map<string, ProviderInfo>
targetPath: string // the scanned directory
installRoot?: string // where node_modules lives, when not targetPath
}installRoot is set only when the two differ. That happens under
--scope changed, which scans a base revision in a throwaway checkout. The
checkout carries no install, so node_modules resolves from the working tree.
The result is the diagnostics, plus any rule that threw:
interface RunRulesResult {
diagnostics: Diagnostic[] // every diagnostic reported
errors: RuleError[] // rules that threw exceptions
}
// Diagnostic is a union of two variants:
type Diagnostic = CodeDiagnostic | SchemaDiagnostic
// Shared base fields
interface DiagnosticBase {
rule: string // e.g. "security/no-eval"
category: Category // "security" | "performance" | "correctness" | "architecture" | "schema"
severity: Severity // "error" | "warning" | "info"
scope?: RuleScope // "file" | "project" | "schema"
filePath: string
message: string // what is wrong
help: string // how to fix it
}
// File-scoped and project-scoped rules emit CodeDiagnostic
interface CodeDiagnostic extends DiagnosticBase {
line: number
column: number
sourceLines?: SourceLine[] // source context around the diagnostic
}
// Schema-scoped rules emit SchemaDiagnostic
interface SchemaDiagnostic extends DiagnosticBase {
entity: string // entity/table name
schemaColumn?: string // column name (if applicable)
}
interface SourceLine {
line: number // 1-based line number
text: string // line content
}How it works
Rule scopes
Every rule has one of three scopes, set by meta.scope.
File-scoped rules (the default) run once per source file:
For each file:
sourceFile = project.getSourceFile(file)
For each file-scoped rule:
context = { sourceFile, filePath, report() }
rule.check(context)Project-scoped rules (scope: "project") run once for the entire project:
For each project-scoped rule:
context = { project, files, moduleGraph, providers, config,
targetPath, installRoot?, report() }
rule.check(context)Schema-scoped rules (scope: "schema") run once per schema graph. They read entity-relation data extracted from Prisma, TypeORM, Drizzle, or MikroORM:
For each schema-scoped rule:
context = { schemaGraph, orm, report() }
rule.check(context)The report() callback
Rules call context.report() to emit diagnostics. The runner auto-fills rule, category, and severity from the rule's metadata:
// Inside a rule's check() method:
context.report({
filePath: context.filePath,
message: "Usage of eval() is a security risk.",
help: "Refactor to avoid eval().",
line: node.getStartLineNumber(),
column: 1,
})
// Runner adds: rule, category, severity from this.metaError handling
Each rule call is wrapped in a try-catch. If a rule throws, the error is recorded and the pipeline continues, so a failing rule never crashes the scan. The recorded shape:
interface RuleErrorInfo {
ruleId: string
error: string
}Rule errors are displayed at the bottom of the console report.
Rule filtering
Before execution, the scanner filters rules based on config:
- Check
config.rules[ruleId].falsedisables the rule. - Check
config.categories[category].falsedisables every rule in that category. - If not explicitly disabled, the rule runs.
Debugging tips
- If a rule is not running, check that it is not disabled in config (
rulesorcategories). - If a rule produces unexpected diagnostics, use the TypeScript AST Viewer to inspect the AST of the code it analyzes.
- Rule errors appear at the bottom of the console report. Check
ruleErrorsin the JSON output for details. - The
report()callback is the only way to emit a diagnostic. A rule that never calls it produces nothing.