Skip to content

AI-Assisted Code Review: Tools and Practices

How to bring AI into code review — automated suggestions, security scanning, review prioritization, and keeping human judgment in the loop.

4 min read
Code review workflow diagram showing AI analysis alongside human reviewer decision points

Code review is one of the highest-leverage activities in software engineering. It catches bugs, spreads knowledge, and maintains code quality. It is also a bottleneck — senior engineers spend hours reviewing PRs instead of building. AI tools can handle the mechanical parts of review so humans focus on design, architecture, and mentorship.

The key is knowing what AI does well (pattern matching, style enforcement, known vulnerability detection) and what it does poorly (understanding business context, evaluating design tradeoffs, assessing team impact).

What AI Handles Well

AI excels at the parts of code review that are tedious but necessary: style consistency, common bug patterns, and known security vulnerabilities.

tstypescript
// ❌ Issues AI catches reliably — humans shouldn't waste time on these
 
// 1. Unused variables
const result = await fetchUser(id);
const data = transformUser(result);
return result; // Should return data, not result
 
// 2. Missing error handling
async function getUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  return response.json(); // No status check
}
 
// 3. SQL injection
const query = `SELECT * FROM users WHERE id = '${userId}'`;
 
// 4. Hardcoded secrets
const API_KEY = 'sk-1234567890abcdef';
 
// 5. Performance anti-patterns
users.forEach(async (user) => {
  await sendEmail(user.email); // Sequential when parallel is safe
});
tstypescript
// ✅ AI-suggested fixes for the above
 
// 1. Correct variable usage
const result = await fetchUser(id);
const data = transformUser(result);
return data;
 
// 2. Proper error handling
async function getUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch user: ${response.status}`);
  }
  return response.json();
}
 
// 3. Parameterized query
const query = 'SELECT * FROM users WHERE id = $1';
const result = await db.query(query, [userId]);
 
// 4. Environment variable
const API_KEY = process.env.API_KEY;
 
// 5. Parallel execution
await Promise.all(users.map(user => sendEmail(user.email)));

These are pattern-based detections. AI does not need to understand your business logic to flag them.

Integrating AI into the Review Pipeline

AI review should run before human review, not after. Let the bot flag mechanical issues so the human reviewer can focus on substance.

ymlyaml
# .github/workflows/ai-review.yml
name: AI Code Review
 
on:
  pull_request:
    types: [opened, synchronize]
 
jobs:
  ai-review:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
 
      - name: Get changed files
        id: diff
        run: |
          FILES=$(git diff --name-only origin/main...HEAD -- '*.ts' '*.tsx')
          echo "files=$FILES" >> "$GITHUB_OUTPUT"
 
      - name: Run static analysis
        run: |
          npx eslint --format json ${{ steps.diff.outputs.files }} > lint-results.json
          npx tsc --noEmit 2> type-errors.txt || true
 
      - name: Security scan
        run: |
          npx audit-ci --high
          npx semgrep --config auto --json ${{ steps.diff.outputs.files }} > security.json
 
      - name: Complexity check
        run: |
          npx ts-complexity --threshold 15 ${{ steps.diff.outputs.files }} > complexity.json
 
      - name: Post review summary
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const lint = JSON.parse(fs.readFileSync('lint-results.json', 'utf8'));
            const security = JSON.parse(fs.readFileSync('security.json', 'utf8'));
 
            let body = '## AI Review Summary\n\n';
            const lintErrors = lint.filter(f => f.errorCount > 0);
            if (lintErrors.length > 0) {
              body += `### Lint Issues: ${lintErrors.length} files\n`;
            }
            if (security.results?.length > 0) {
              body += `### Security Findings: ${security.results.length}\n`;
            }
 
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: body || '## AI Review Summary\n\n✅ No issues found'
            });

The pipeline runs linting, type checking, security scanning, and complexity analysis. Results are posted as a PR comment before a human reviewer looks at the code.

Review Prioritization

Not all PRs need the same level of scrutiny. AI can triage PRs by risk level so reviewers prioritize correctly.

tstypescript
interface PRRiskAssessment {
  riskLevel: 'low' | 'medium' | 'high' | 'critical';
  factors: string[];
  suggestedReviewers: string[];
  estimatedReviewTime: string;
}
 
function assessPRRisk(pr: PullRequestData): PRRiskAssessment {
  const factors: string[] = [];
  let riskScore = 0;
 
  // File change patterns
  if (pr.changedFiles.some(f => f.includes('migration'))) {
    factors.push('Database migration detected');
    riskScore += 3;
  }
 
  if (pr.changedFiles.some(f => f.includes('auth') || f.includes('security'))) {
    factors.push('Security-sensitive files modified');
    riskScore += 3;
  }
 
  if (pr.changedFiles.some(f => f.includes('.env') || f.includes('config'))) {
    factors.push('Configuration changes');
    riskScore += 2;
  }
 
  // Scale of change
  if (pr.additions + pr.deletions > 500) {
    factors.push(`Large change: ${pr.additions + pr.deletions} lines`);
    riskScore += 2;
  }
 
  // New dependencies
  if (pr.changedFiles.includes('package.json')) {
    factors.push('Dependency changes');
    riskScore += 1;
  }
 
  // API surface changes
  if (pr.changedFiles.some(f => f.match(/routes|controller|handler/))) {
    factors.push('API surface modified');
    riskScore += 2;
  }
 
  const riskLevel = riskScore >= 6 ? 'critical'
    : riskScore >= 4 ? 'high'
    : riskScore >= 2 ? 'medium'
    : 'low';
 
  return {
    riskLevel,
    factors,
    suggestedReviewers: getSuggestedReviewers(pr.changedFiles),
    estimatedReviewTime: riskScore >= 4 ? '30-60 min' : '10-20 min',
  };
}
 
function getSuggestedReviewers(files: string[]): string[] {
  // Map files to code owners or recent contributors
  const reviewers = new Set<string>();
  for (const file of files) {
    if (file.startsWith('src/auth/')) reviewers.add('security-team');
    if (file.startsWith('src/api/')) reviewers.add('api-team');
    if (file.includes('migration')) reviewers.add('dba-team');
  }
  return Array.from(reviewers);
}

A "critical" PR (database migration + auth changes + large diff) gets routed to senior reviewers immediately. A "low" PR (documentation update, 10 lines) can be reviewed by anyone or auto-merged after CI passes.

What Humans Must Still Review

AI cannot replace human judgment for design decisions, business logic correctness, and team context.

markdownmarkdown
## Human-Only Review Checklist
 
### Architecture & Design
- [ ] Does the approach make sense for this problem?
- [ ] Are there simpler alternatives that were not considered?
- [ ] Does this create technical debt we will regret?
- [ ] Does this align with the team's established patterns?
 
### Business Logic
- [ ] Does this correctly implement the requirements?
- [ ] Are edge cases from the domain handled?
- [ ] Will this break existing user workflows?
 
### Team Impact
- [ ] Will other team members understand this code?
- [ ] Does this increase or decrease on-call burden?
- [ ] Is the testing approach appropriate for the risk?
 
### Context
- [ ] Does the PR description explain WHY, not just WHAT?
- [ ] Are there related PRs that should be reviewed together?
- [ ] Does this need feature flag protection?
tstypescript
// ❌ AI would approve this — it's syntactically correct and has no bugs
async function processOrder(order: Order): Promise<void> {
  await chargePayment(order.total);
  await updateInventory(order.items);
  await sendConfirmation(order.customerEmail);
}
// But a human reviewer asks: What happens if chargePayment succeeds
// but updateInventory fails? The customer is charged but items aren't
// reserved. This needs a transaction or a saga pattern.
 
// ✅ Human insight leads to a more resilient design
async function processOrder(order: Order): Promise<void> {
  const paymentId = await chargePayment(order.total);
 
  try {
    await updateInventory(order.items);
  } catch (error) {
    await refundPayment(paymentId);
    throw new OrderProcessingError('Inventory update failed', { paymentId });
  }
 
  await sendConfirmation(order.customerEmail).catch(err => {
    // Email failure is non-critical — log and continue
    logger.error({ orderId: order.id, err }, 'Confirmation email failed');
  });
}

AI sees correct TypeScript. A human sees a consistency problem that requires domain knowledge to identify.

Key Takeaways

  1. Let AI handle mechanical review — style issues, common bugs, security patterns, and unused code
  2. Run AI analysis before human review — post findings as PR comments so reviewers skip trivial issues
  3. Triage PRs by risk level — route database migrations and auth changes to senior reviewers, auto-merge documentation fixes
  4. Humans own design review — architecture decisions, business logic correctness, and team impact require context AI lacks
  5. AI augments, it does not replace — the best workflow combines AI speed with human judgment
  6. Invest in the feedback loop — when AI suggestions are wrong, improve the rules; when humans catch patterns repeatedly, automate them
Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX