Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, RuleFunction> = {
'no-relative-paths': noRelativePaths,
Expand Down Expand Up @@ -69,4 +70,5 @@ export const rules: Record<string, RuleFunction> = {
'prefer-guard-clauses': preferGuardClauses,
'no-type-assertion': noTypeAssertion,
'no-manual-retry-loop': noManualRetryLoop,
'no-placeholder-comments': noPlaceholderComments,
};
50 changes: 50 additions & 0 deletions src/rules/no-placeholder-comments.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion tests/config-modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
67 changes: 67 additions & 0 deletions tests/no-placeholder-comments.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to handle JSX comments too?

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);
});
});
Loading