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.meta

Error 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:

  1. Check config.rules[ruleId]. false disables the rule.
  2. Check config.categories[category]. false disables every rule in that category.
  3. If not explicitly disabled, the rule runs.

Debugging tips

  • If a rule is not running, check that it is not disabled in config (rules or categories).
  • 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 ruleErrors in the JSON output for details.
  • The report() callback is the only way to emit a diagnostic. A rule that never calls it produces nothing.