From dd04e5268312e1a2ab4ffa5c8b3dd0ebb3aee308 Mon Sep 17 00:00:00 2001 From: Arnav Surve Date: Fri, 13 Feb 2026 18:21:44 -0800 Subject: [PATCH] Add no-placeholder-comments rule Detects TODO, FIXME, "implement this", "add your code here", and other placeholder comments commonly left behind by AI code generators. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 6 +++ src/rules/index.ts | 2 + src/rules/no-placeholder-comments.ts | 50 ++++++++++++++++++++ tests/config-modes.test.ts | 2 +- tests/no-placeholder-comments.test.ts | 67 +++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/rules/no-placeholder-comments.ts create mode 100644 tests/no-placeholder-comments.test.ts diff --git a/README.md b/README.md index 1568c12..a2a2185 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,12 @@ const ruleNames = getAllRuleNames(); // ['no-relative-paths', 'expo-image-import | --------------------- | -------- | --------------------------------------------- | | `prefer-lucide-icons` | warning | Prefer lucide-react/lucide-react-native icons | +### Agent Code Quality Rules + +| Rule | Severity | Description | +| ------------------------- | -------- | -------------------------------------------------------------- | +| `no-placeholder-comments` | warning | Detect TODO, FIXME, and placeholder comments left by AI agents | + --- ## Rule Details diff --git a/src/rules/index.ts b/src/rules/index.ts index e8f4d36..24f8392 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -33,6 +33,7 @@ import { transitionPreferBlankStack } from './transition-prefer-blank-stack'; import { preferGuardClauses } from './prefer-guard-clauses'; import { noTypeAssertion } from './no-type-assertion'; import { noManualRetryLoop } from './no-manual-retry-loop'; +import { noPlaceholderComments } from './no-placeholder-comments'; export const rules: Record = { 'no-relative-paths': noRelativePaths, @@ -69,4 +70,5 @@ export const rules: Record = { 'prefer-guard-clauses': preferGuardClauses, 'no-type-assertion': noTypeAssertion, 'no-manual-retry-loop': noManualRetryLoop, + 'no-placeholder-comments': noPlaceholderComments, }; diff --git a/src/rules/no-placeholder-comments.ts b/src/rules/no-placeholder-comments.ts new file mode 100644 index 0000000..0b821e4 --- /dev/null +++ b/src/rules/no-placeholder-comments.ts @@ -0,0 +1,50 @@ +import type { File } from '@babel/types'; +import type { LintResult } from '../types'; + +const RULE_NAME = 'no-placeholder-comments'; + +const PLACEHOLDER_PATTERNS = [ + /\bTODO\b/i, + /\bFIXME\b/i, + /\bHACK\b/i, + /\bXXX\b/i, + /\badd\s+your\b/i, + /\bimplement\s+(this|here|me)\b/i, + /\bplaceholder\b/i, + /\breplace\s+(this|me|with)\b/i, + /\bfill\s+in\b/i, + /\byour\s+(code|logic|implementation)\s+here\b/i, + /\bchange\s+this\b/i, + /\binsert\s+(here|your)\b/i, +]; + +export function noPlaceholderComments(ast: File, code: string): LintResult[] { + const results: LintResult[] = []; + const lines = code.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // Match single-line comments (//) and multi-line comment content + const commentMatch = + line.match(/\/\/(.*)$/) || line.match(/\/\*(.*)/) || line.match(/^\s*\*\s?(.*)/); + if (!commentMatch) continue; + + const commentText = commentMatch[1]; + if (!commentText) continue; + + for (const pattern of PLACEHOLDER_PATTERNS) { + if (pattern.test(commentText)) { + results.push({ + rule: RULE_NAME, + message: `Remove placeholder comment: "${commentText.trim()}"`, + line: i + 1, + column: 0, + severity: 'warning', + }); + break; + } + } + } + + return results; +} diff --git a/tests/config-modes.test.ts b/tests/config-modes.test.ts index 51cdb4e..2a3c309 100644 --- a/tests/config-modes.test.ts +++ b/tests/config-modes.test.ts @@ -100,7 +100,7 @@ describe('config modes', () => { expect(ruleNames).toContain('no-relative-paths'); expect(ruleNames).toContain('expo-image-import'); expect(ruleNames).toContain('no-stylesheet-create'); - expect(ruleNames.length).toBe(34); + expect(ruleNames.length).toBe(35); }); }); }); diff --git a/tests/no-placeholder-comments.test.ts b/tests/no-placeholder-comments.test.ts new file mode 100644 index 0000000..9942813 --- /dev/null +++ b/tests/no-placeholder-comments.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { lintJsxCode } from '../src'; + +const config = { rules: ['no-placeholder-comments'] }; + +describe('no-placeholder-comments rule', () => { + it('should detect TODO comments', () => { + const code = `// TODO: implement this later`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + expect(results[0].rule).toBe('no-placeholder-comments'); + }); + + it('should detect FIXME comments', () => { + const code = `// FIXME: broken logic`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + }); + + it('should detect "add your code here" comments', () => { + const code = `// Add your logic here`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + }); + + it('should detect "implement this" comments', () => { + const code = `// implement this`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + }); + + it('should detect placeholder comments', () => { + const code = `// placeholder for auth logic`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + }); + + it('should detect "replace this" comments', () => { + const code = `// replace this with real implementation`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + }); + + it('should detect "fill in" comments', () => { + const code = `// fill in the details`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + }); + + it('should not flag regular comments', () => { + const code = `// This component handles user authentication`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(0); + }); + + it('should not flag code without comments', () => { + const code = `const x = 5;`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(0); + }); + + it('should detect comments in multi-line blocks', () => { + const code = `/* TODO: fix this */`; + const results = lintJsxCode(code, config); + expect(results).toHaveLength(1); + }); +});