nestjs-doctorGitHub

Rules Overview

nestjs-doctor ships with 43 built-in rules across five categories.

Categories

CategoryRulesFocus
Security9Secrets, injection, CSRF, stack traces
Correctness14Missing decorators, duplicate routes, async issues
Architecture10Layer violations, circular deps, DI patterns
Performance7Sync I/O, blocking constructors, dead code
Schema3Primary keys, timestamps, cascade rules

Rule Scopes

Rules have one of three scopes:

  • File-scoped (default) — runs once per source file. Gets a RuleContext with the file's AST.
  • Project-scoped (scope: "project") — runs once for the entire project. Gets a ProjectRuleContext with the full module graph and provider map.
  • Schema-scoped (scope: "schema") — runs once per schema graph. Gets a SchemaRuleContext with the extracted entity-relation data from Prisma or TypeORM.

Rule Structure

Every rule has a meta object and a check function:

import type { Rule } from "../../types.js";

export const myRule: Rule = {
  meta: {
    id: "category/my-rule",          // unique ID
    category: "correctness",         // security | correctness | architecture | performance | schema
    severity: "warning",             // error | warning | info
    description: "What this rule detects",
    help: "How to fix it",
  },

  check(context) {
    // Inspect context.sourceFile using ts-morph API
    // Call context.report() to emit a diagnostic
  },
}

Writing a New Rule

1. Create the rule file

Create src/engine/rules/definitions/<category>/my-rule.ts:

import type { Rule } from "../../types.js";
import { SyntaxKind } from "ts-morph";

export const noFoo: Rule = {
  meta: {
    id: "correctness/no-foo",
    category: "correctness",
    severity: "warning",
    description: "Detects usage of foo()",
    help: "Replace foo() with bar().",
  },

  check(context) {
    const calls = context.sourceFile.getDescendantsOfKind(
      SyntaxKind.CallExpression
    );

    for (const call of calls) {
      if (call.getExpression().getText() === "foo") {
        context.report({
          filePath: context.filePath,
          message: "Usage of foo() detected.",
          help: this.meta.help,
          line: call.getStartLineNumber(),
          column: 1,
        });
      }
    }
  },
};

2. Register it

Add the import and reference in src/engine/rules/index.ts:

import { noFoo } from "./definitions/correctness/no-foo.js";

export const allRules: AnyRule[] = [
  // ... existing rules
  noFoo,
];

3. Add tests

Create tests/unit/rules/correctness/no-foo.test.ts.

Project-Scoped Rules

For rules that need the full module graph or provider map:

import type { ProjectRule } from "../../types.js";

export const noOrphanServices: ProjectRule = {
  meta: {
    id: "architecture/no-orphan-services",
    category: "architecture",
    severity: "info",
    description: "Services not registered in any module",
    help: "Add the service to a module's providers array.",
    scope: "project",  // This makes it project-scoped
  },

  check(context) {
    // context.moduleGraph — full module dependency graph
    // context.providers — Map<string, ProviderInfo>
    // context.project — ts-morph Project
    // context.files — all file paths
    // context.config — user config
  },
};

Context API

RuleContext (file-scoped)

PropertyTypeDescription
sourceFileSourceFilets-morph SourceFile for the current file
filePathstringAbsolute path to the current file
configNestjsDoctorConfig | undefinedUser configuration (optional)
report()(diagnostic) => voidEmit a diagnostic

ProjectRuleContext (project-scoped)

PropertyTypeDescription
projectProjectts-morph Project with all source files
filesstring[]All file paths being scanned
moduleGraphModuleGraphModule dependency graph
providersMap<string, ProviderInfo>All @Injectable() classes
configNestjsDoctorConfigUser configuration
report()(diagnostic) => voidEmit a diagnostic

SchemaRuleContext (schema-scoped)

PropertyTypeDescription
schemaGraphSchemaGraphEntity-relation data extracted from Prisma or TypeORM
ormstringDetected ORM name (e.g. "prisma", "typeorm")
report()(diagnostic) => voidEmit a diagnostic

report() Fields

context.report({
  filePath: string,    // which file the issue is in
  message: string,     // what's wrong
  help: string,        // how to fix it
  line: number,        // line number (1-based)
  column: number,      // column number (1-based)
});
// rule, category, severity are auto-filled from meta