diff --git a/source/skills/critique/scripts/detect-antipatterns.mjs b/source/skills/critique/scripts/detect-antipatterns.mjs
index 7c0e76d04..e4e8e0cca 100644
--- a/source/skills/critique/scripts/detect-antipatterns.mjs
+++ b/source/skills/critique/scripts/detect-antipatterns.mjs
@@ -1945,31 +1945,108 @@ const REGEX_ANALYZERS = [
},
];
-function detectText(content, filePath) {
- const findings = [];
- const lines = content.split('\n');
+// ---------------------------------------------------------------------------
+// Style block extraction (Vue/Svelte `;
+ const blocks = extractStyleBlocks(vue, '.vue');
+ expect(blocks.length).toBe(1);
+ expect(blocks[0].content).toContain('border-left: 4px solid blue');
+ expect(blocks[0].startLine).toBeGreaterThan(1);
+ });
+
+ test('extracts multiple
+`;
+ const blocks = extractStyleBlocks(vue, '.vue');
+ expect(blocks.length).toBe(2);
+ });
+
+ test('extracts `;
+ const blocks = extractStyleBlocks(svelte, '.svelte');
+ expect(blocks.length).toBe(1);
+ expect(blocks[0].content).toContain('border-right: 4px solid');
+ });
+
+ test('returns empty for non-Vue/Svelte files', () => {
+ const jsx = 'export function Card() { return
hi
; }';
+ expect(extractStyleBlocks(jsx, '.jsx')).toHaveLength(0);
+ expect(extractStyleBlocks(jsx, '.tsx')).toHaveLength(0);
+ });
+
+ test('returns empty when no `;
+ const f = detectText(vue, 'Card.vue');
+ expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
+ });
+
+ test('detects overused font in `;
+ const f = detectText(vue, 'App.vue');
+ expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
+ });
+
+ test('detects bounce animation in `;
+ const f = detectText(vue, 'Card.vue');
+ expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true);
+ });
+
+ test('detects gradient-text in `;
+ const f = detectText(vue, 'Hero.vue');
+ expect(f.some(r => r.antipattern === 'gradient-text')).toBe(true);
+ });
+
+ test('detects Tailwind anti-patterns in ', () => {
+ const vue = `
+ card
+`;
+ const f = detectText(vue, 'Card.vue');
+ expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
+ });
+});
+
+describe('detectText -- Svelte', () => {
+ test('detects side-tab in `;
+ const f = detectText(svelte, 'Sidebar.svelte');
+ expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
+ });
+
+ test('detects overused font in `;
+ const f = detectText(svelte, 'App.svelte');
+ expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
+ });
+
+ test('detects layout transition in `;
+ const f = detectText(svelte, 'Panel.svelte');
+ expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Tier 1: detectText on CSS-in-JS files
+// ---------------------------------------------------------------------------
+
+describe('detectText -- CSS-in-JS', () => {
+ test('detects side-tab in styled-components', () => {
+ const tsx = "const Card = styled.div`\n border-left: 4px solid #3b82f6;\n border-radius: 12px;\n`;";
+ const f = detectText(tsx, 'Card.tsx');
+ expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
+ });
+
+ test('detects bounce in emotion css', () => {
+ const tsx = "const style = css`\n animation: bounce 1s infinite;\n`;";
+ const f = detectText(tsx, 'anim.ts');
+ expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true);
+ });
+
+ test('detects overused font in styled-components', () => {
+ const tsx = "const Wrapper = styled.main`\n font-family: 'Inter', sans-serif;\n`;";
+ const f = detectText(tsx, 'Layout.tsx');
+ expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
+ });
+
+ test('detects gradient-text in styled-components', () => {
+ const tsx = "const Title = styled.h1`\n background: linear-gradient(to right, purple, cyan);\n -webkit-background-clip: text;\n background-clip: text;\n`;";
+ const f = detectText(tsx, 'Hero.tsx');
+ expect(f.some(r => r.antipattern === 'gradient-text')).toBe(true);
+ });
+
+ test('detects pure-black-white in styled-components', () => {
+ const tsx = "const Dark = styled.section`\n background-color: #000000;\n`;";
+ const f = detectText(tsx, 'Dark.tsx');
+ expect(f.some(r => r.antipattern === 'pure-black-white')).toBe(true);
+ });
+
+ test('does not false-positive on clean CSS-in-JS', () => {
+ const tsx = "const Card = styled.div`\n border-radius: 12px;\n padding: 24px;\n`;";
+ const f = detectText(tsx, 'Card.tsx');
+ expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Tier 1: Fixture file integration tests (CLI)
+// ---------------------------------------------------------------------------
+
+describe('CLI -- framework fixtures', () => {
+ function run(...args) {
+ const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
+ return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
+ }
+
+ test('jsx-should-flag catches anti-patterns', () => {
+ const { code, stderr } = run(path.join(FIXTURES, 'jsx-should-flag.jsx'));
+ expect(code).toBe(2);
+ expect(stderr).toContain('side-tab');
+ });
+
+ test('jsx-should-pass is clean', () => {
+ const { code } = run(path.join(FIXTURES, 'jsx-should-pass.jsx'));
+ expect(code).toBe(0);
+ });
+
+ test('vue-should-flag catches anti-patterns', () => {
+ const { code, stderr } = run(path.join(FIXTURES, 'vue-should-flag.vue'));
+ expect(code).toBe(2);
+ expect(stderr).toContain('side-tab');
+ });
+
+ test('vue-should-pass is clean', () => {
+ const { code } = run(path.join(FIXTURES, 'vue-should-pass.vue'));
+ expect(code).toBe(0);
+ });
+
+ test('svelte-should-flag catches anti-patterns', () => {
+ const { code, stderr } = run(path.join(FIXTURES, 'svelte-should-flag.svelte'));
+ expect(code).toBe(2);
+ expect(stderr).toContain('side-tab');
+ });
+
+ test('svelte-should-pass is clean', () => {
+ const { code } = run(path.join(FIXTURES, 'svelte-should-pass.svelte'));
+ expect(code).toBe(0);
+ });
+
+ test('cssinjs-should-flag catches anti-patterns', () => {
+ const { code, stderr } = run(path.join(FIXTURES, 'cssinjs-should-flag.tsx'));
+ expect(code).toBe(2);
+ expect(stderr).toContain('side-tab');
+ });
+
+ test('cssinjs-should-pass is clean', () => {
+ const { code } = run(path.join(FIXTURES, 'cssinjs-should-pass.tsx'));
+ expect(code).toBe(0);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Realistic Next.js project fixtures
+// ---------------------------------------------------------------------------
+
+describe('CLI -- Next.js + Tailwind project', () => {
+ const dir = path.join(FIXTURES, 'framework-next-tailwind');
+ let stderr;
+
+ function run(...args) {
+ const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
+ return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
+ }
+
+ test('finds all expected anti-pattern types', () => {
+ const result = run(dir);
+ stderr = result.stderr;
+ expect(result.code).toBe(2);
+ for (const ap of ['side-tab', 'gradient-text', 'ai-color-palette', 'overused-font', 'bounce-easing', 'pure-black-white']) {
+ expect(stderr).toContain(ap);
+ }
+ });
+
+ test('FeatureCard: side-tab + ai-color-palette + bounce-easing', () => {
+ const { stderr } = run(path.join(dir, 'components', 'FeatureCard.tsx'));
+ expect(stderr).toContain('side-tab');
+ expect(stderr).toContain('border-l-4');
+ expect(stderr).toContain('ai-color-palette');
+ expect(stderr).toContain('text-purple-600');
+ expect(stderr).toContain('bounce-easing');
+ expect(stderr).toContain('animate-bounce');
+ });
+
+ test('PricingCard: pure-black-white + gradient-text + ai-color-palette', () => {
+ const { stderr } = run(path.join(dir, 'components', 'PricingCard.tsx'));
+ expect(stderr).toContain('pure-black-white');
+ expect(stderr).toContain('bg-black');
+ expect(stderr).toContain('gradient-text');
+ expect(stderr).toContain('bg-clip-text');
+ expect(stderr).toContain('ai-color-palette');
+ });
+
+ test('globals.css: overused Inter font', () => {
+ const { stderr } = run(path.join(dir, 'app', 'globals.css'));
+ expect(stderr).toContain('overused-font');
+ expect(stderr).toContain('Inter');
+ });
+
+ test('page.tsx: gradient-text + ai-color-palette', () => {
+ const { stderr } = run(path.join(dir, 'app', 'page.tsx'));
+ expect(stderr).toContain('gradient-text');
+ expect(stderr).toContain('ai-color-palette');
+ });
+
+ test('directory scan shows import context for components', () => {
+ const { stderr } = run(dir);
+ expect(stderr).toContain('imported by page.tsx');
+ });
+
+ test('--json produces clean JSON without framework message', () => {
+ const { stderr, code } = run('--json', dir);
+ expect(code).toBe(2);
+ const parsed = JSON.parse(stderr.trim());
+ expect(parsed).toBeArray();
+ expect(parsed.length).toBeGreaterThanOrEqual(6);
+ });
+});
+
+describe('CLI -- Next.js + CSS Modules project', () => {
+ function run(...args) {
+ const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
+ return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
+ }
+
+ const dir = path.join(FIXTURES, 'framework-next-modules');
+
+ test('finds all expected anti-pattern types', () => {
+ const { code, stderr } = run(dir);
+ expect(code).toBe(2);
+ for (const ap of ['side-tab', 'overused-font', 'pure-black-white', 'layout-transition', 'gradient-text']) {
+ expect(stderr).toContain(ap);
+ }
+ });
+
+ test('StatsCard.module.css: side-tab + overused-font + layout-transition', () => {
+ const { stderr } = run(path.join(dir, 'components', 'StatsCard.module.css'));
+ expect(stderr).toContain('side-tab');
+ expect(stderr).toContain('border-left: 4px solid #6366f1');
+ expect(stderr).toContain('overused-font');
+ expect(stderr).toContain('Inter');
+ expect(stderr).toContain('layout-transition');
+ expect(stderr).toContain('transition: width');
+ });
+
+ test('Sidebar.module.css: side-tab border accent', () => {
+ const { stderr } = run(path.join(dir, 'components', 'Sidebar.module.css'));
+ expect(stderr).toContain('side-tab');
+ expect(stderr).toContain('border-right: 3px solid');
+ });
+
+ test('globals.css: overused Roboto + pure-black-white', () => {
+ const { stderr } = run(path.join(dir, 'app', 'globals.css'));
+ expect(stderr).toContain('overused-font');
+ expect(stderr).toContain('Roboto');
+ expect(stderr).toContain('pure-black-white');
+ expect(stderr).toContain('#000000');
+ });
+
+ test('page.module.css: gradient-text across lines', () => {
+ const { stderr } = run(path.join(dir, 'app', 'page.module.css'));
+ expect(stderr).toContain('gradient-text');
+ expect(stderr).toContain('background-clip: text');
+ });
+
+ test('directory scan shows import context for CSS modules', () => {
+ const { stderr } = run(dir);
+ expect(stderr).toContain('imported by StatsCard.tsx');
+ expect(stderr).toContain('imported by Sidebar.tsx');
+ expect(stderr).toContain('imported by layout.tsx');
+ });
+});
+
+describe('CLI -- Next.js + CSS-in-JS (styled-components) project', () => {
+ function run(...args) {
+ const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
+ return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
+ }
+
+ const dir = path.join(FIXTURES, 'framework-next-cssinjs');
+
+ test('finds all expected anti-pattern types', () => {
+ const { code, stderr } = run(dir);
+ expect(code).toBe(2);
+ for (const ap of ['side-tab', 'gradient-text', 'overused-font', 'bounce-easing', 'pure-black-white', 'layout-transition']) {
+ expect(stderr).toContain(ap);
+ }
+ });
+
+ test('FeatureGrid.tsx: side-tab + bounce-easing + layout-transition', () => {
+ const { stderr } = run(path.join(dir, 'components', 'FeatureGrid.tsx'));
+ expect(stderr).toContain('side-tab');
+ expect(stderr).toContain('border-left: 4px solid');
+ expect(stderr).toContain('bounce-easing');
+ expect(stderr).toContain('animation: bounce');
+ expect(stderr).toContain('layout-transition');
+ expect(stderr).toContain('transition: width');
+ });
+
+ test('Hero.tsx: gradient-text + overused Montserrat font', () => {
+ const { stderr } = run(path.join(dir, 'components', 'Hero.tsx'));
+ expect(stderr).toContain('gradient-text');
+ expect(stderr).toContain('background-clip: text');
+ expect(stderr).toContain('overused-font');
+ expect(stderr).toContain('Montserrat');
+ });
+
+ test('GlobalStyle.tsx: overused Inter + pure-black-white', () => {
+ const { stderr } = run(path.join(dir, 'components', 'GlobalStyle.tsx'));
+ expect(stderr).toContain('overused-font');
+ expect(stderr).toContain('Inter');
+ expect(stderr).toContain('pure-black-white');
+ expect(stderr).toContain('#000000');
+ });
+
+ test('Testimonials.tsx: side-tab + gradient-text in styled blockquote', () => {
+ const { stderr } = run(path.join(dir, 'components', 'Testimonials.tsx'));
+ expect(stderr).toContain('side-tab');
+ expect(stderr).toContain('border-left: 4px solid');
+ expect(stderr).toContain('gradient-text');
+ });
+
+ test('directory scan shows import context for components', () => {
+ const { stderr } = run(dir);
+ expect(stderr).toContain('imported by index.tsx');
+ expect(stderr).toContain('imported by _app.tsx');
+ });
+
+ test('--json produces clean JSON without framework message', () => {
+ const { stderr, code } = run('--json', dir);
+ expect(code).toBe(2);
+ const parsed = JSON.parse(stderr.trim());
+ expect(parsed).toBeArray();
+ expect(parsed.length).toBeGreaterThanOrEqual(6);
+ // Verify importedBy is present in JSON
+ const featureGridFindings = parsed.filter(f => f.file?.includes('FeatureGrid'));
+ expect(featureGridFindings.length).toBeGreaterThan(0);
+ expect(featureGridFindings[0].importedBy).toContain('index.tsx');
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Tier 2: Import graph
+// ---------------------------------------------------------------------------
+
+describe('buildImportGraph', () => {
+ const MF = path.join(FIXTURES, 'multifile');
+
+ test('resolves ES import from tsx to tsx', () => {
+ const graph = buildImportGraph([
+ path.join(MF, 'App.tsx'),
+ path.join(MF, 'Card.tsx'),
+ path.join(MF, 'styles.css'),
+ ]);
+ const appImports = graph.get(path.join(MF, 'App.tsx'));
+ expect(appImports).toBeDefined();
+ expect(appImports.has(path.join(MF, 'Card.tsx'))).toBe(true);
+ expect(appImports.has(path.join(MF, 'styles.css'))).toBe(true);
+ });
+
+ test('resolves extensionless imports', () => {
+ const graph = buildImportGraph([
+ path.join(MF, 'App.tsx'),
+ path.join(MF, 'Card.tsx'),
+ ]);
+ const appImports = graph.get(path.join(MF, 'App.tsx'));
+ expect(appImports.has(path.join(MF, 'Card.tsx'))).toBe(true);
+ });
+
+ test('resolves CSS @import', () => {
+ const graph = buildImportGraph([
+ path.join(MF, 'theme.scss'),
+ path.join(MF, 'variables.scss'),
+ ]);
+ const themeImports = graph.get(path.join(MF, 'theme.scss'));
+ expect(themeImports).toBeDefined();
+ expect(themeImports.has(path.join(MF, 'variables.scss'))).toBe(true);
+ });
+
+ test('ignores bare/node_modules imports', () => {
+ const graph = buildImportGraph([
+ path.join(MF, 'App.tsx'),
+ ]);
+ const appImports = graph.get(path.join(MF, 'App.tsx'));
+ // Should not contain 'react' or 'styled-components'
+ for (const imp of appImports) {
+ expect(imp).toContain(MF);
+ }
+ });
+});
+
+describe('resolveImport', () => {
+ const MF = path.join(FIXTURES, 'multifile');
+
+ test('resolves relative path with extension', () => {
+ const fileSet = new Set([path.join(MF, 'Card.tsx')]);
+ const result = resolveImport('./Card.tsx', MF, fileSet);
+ expect(result).toBe(path.join(MF, 'Card.tsx'));
+ });
+
+ test('resolves extensionless import by trying extensions', () => {
+ const fileSet = new Set([path.join(MF, 'Card.tsx')]);
+ const result = resolveImport('./Card', MF, fileSet);
+ expect(result).toBe(path.join(MF, 'Card.tsx'));
+ });
+
+ test('returns null for bare specifiers', () => {
+ const fileSet = new Set([path.join(MF, 'Card.tsx')]);
+ expect(resolveImport('react', MF, fileSet)).toBeNull();
+ expect(resolveImport('styled-components', MF, fileSet)).toBeNull();
+ });
+
+ test('returns null for unresolvable imports', () => {
+ const fileSet = new Set([path.join(MF, 'Card.tsx')]);
+ expect(resolveImport('./Unknown', MF, fileSet)).toBeNull();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Tier 2: Multi-file directory scan
+// ---------------------------------------------------------------------------
+
+describe('CLI -- multi-file scan', () => {
+ function run(...args) {
+ const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
+ return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
+ }
+
+ test('scanning multifile/ directory finds findings across files', () => {
+ const { code, stderr } = run(path.join(FIXTURES, 'multifile'));
+ expect(code).toBe(2);
+ expect(stderr).toContain('side-tab');
+ });
+
+ test('--json multi-file scan includes import context', () => {
+ const { stderr, code } = run('--json', path.join(FIXTURES, 'multifile'));
+ expect(code).toBe(2);
+ const parsed = JSON.parse(stderr.trim());
+ expect(parsed.length).toBeGreaterThan(0);
+ // Findings from Card.tsx should mention being imported by App.tsx
+ const cardFindings = parsed.filter(f => f.file?.includes('Card.tsx'));
+ expect(cardFindings.length).toBeGreaterThan(0);
+ expect(cardFindings.some(f => f.importedBy?.includes('App.tsx'))).toBe(true);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Tier 3: Framework config detection
+// ---------------------------------------------------------------------------
+
+describe('detectFrameworkConfig', () => {
+ test('detects next.config.mjs and returns Next.js with default port', () => {
+ const result = detectFrameworkConfig(path.join(FIXTURES, 'framework-next-tailwind'));
+ expect(result).not.toBeNull();
+ expect(result.name).toBe('Next.js');
+ expect(result.port).toBe(3000);
+ });
+
+ test('detects next.config.js (pages router)', () => {
+ const result = detectFrameworkConfig(path.join(FIXTURES, 'framework-next-cssinjs'));
+ expect(result).not.toBeNull();
+ expect(result.name).toBe('Next.js');
+ });
+
+ test('parses custom port from vite.config.ts', () => {
+ const result = detectFrameworkConfig(path.join(FIXTURES, 'framework-vite'));
+ expect(result).not.toBeNull();
+ expect(result.name).toBe('Vite');
+ expect(result.port).toBe(8080);
+ });
+
+ test('returns null for directory without framework config', () => {
+ const result = detectFrameworkConfig(path.join(FIXTURES, 'multifile'));
+ expect(result).toBeNull();
+ });
+
+ test('returns null for nonexistent directory', () => {
+ const result = detectFrameworkConfig('/nonexistent/path/12345');
+ expect(result).toBeNull();
+ });
+});
+
+describe('isPortListening', () => {
+ test('returns { listening: false } for unlikely port', async () => {
+ const result = await isPortListening(59999);
+ expect(result.listening).toBe(false);
+ });
+});
+
+describe('FRAMEWORK_CONFIGS', () => {
+ test('covers major frameworks', () => {
+ const names = FRAMEWORK_CONFIGS.map(c => c.name);
+ expect(names).toContain('Next.js');
+ expect(names).toContain('Vite');
+ expect(names).toContain('SvelteKit');
+ expect(names).toContain('Nuxt');
+ expect(names).toContain('Astro');
+ });
+
+ test('each config has required fields', () => {
+ for (const cfg of FRAMEWORK_CONFIGS) {
+ expect(cfg.name).toBeTypeOf('string');
+ expect(cfg.defaultPort).toBeTypeOf('number');
+ expect(cfg.files).toBeArray();
+ expect(cfg.files.length).toBeGreaterThan(0);
+ }
+ });
+});
+
+describe('CLI -- dev server suggestion', () => {
+ function run(...args) {
+ const result = spawnSync('node', [SCRIPT, ...args], { encoding: 'utf-8', timeout: 15000 });
+ return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.status };
+ }
+
+ test('suggests URL scan when Next.js config found', () => {
+ const { stderr } = run(path.join(FIXTURES, 'framework-next-tailwind'));
+ expect(stderr).toContain('Next.js');
+ expect(stderr).toContain('3000');
+ });
+
+ test('suggests URL scan when Vite config found', () => {
+ const { stderr } = run(path.join(FIXTURES, 'framework-vite'));
+ expect(stderr).toContain('Vite');
+ expect(stderr).toContain('8080');
+ });
+});
diff --git a/tests/fixtures/antipatterns/cssinjs-should-flag.tsx b/tests/fixtures/antipatterns/cssinjs-should-flag.tsx
new file mode 100644
index 000000000..eee8692d6
--- /dev/null
+++ b/tests/fixtures/antipatterns/cssinjs-should-flag.tsx
@@ -0,0 +1,44 @@
+// CSS-in-JS patterns with anti-patterns (styled-components + emotion)
+
+import styled from 'styled-components';
+import { css } from '@emotion/react';
+
+// styled-components: side-tab + border-accent-on-rounded
+export const Card = styled.div`
+ border-left: 4px solid #3b82f6;
+ border-radius: 12px;
+ padding: 24px;
+ font-family: 'Inter', sans-serif;
+`;
+
+// styled-components: pure black background + gradient text
+export const Hero = styled.section`
+ background-color: #000000;
+ padding: 80px 20px;
+ text-align: center;
+
+ h1 {
+ background: linear-gradient(to right, #a855f7, #06b6d4);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ }
+`;
+
+// emotion css: bounce animation + layout transition
+export const animatedPanel = css`
+ animation: bounce 1s infinite;
+ transition: width 0.3s ease;
+`;
+
+// styled with parenthesized component
+export const AccentBox = styled(Box)`
+ border-right: 5px solid #8b5cf6;
+ border-radius: 8px;
+`;
+
+// Object style pattern
+export const inlineStyles = {
+ borderLeft: '4px solid #6366f1',
+ borderRadius: '12px',
+};
diff --git a/tests/fixtures/antipatterns/cssinjs-should-pass.tsx b/tests/fixtures/antipatterns/cssinjs-should-pass.tsx
new file mode 100644
index 000000000..161967ce8
--- /dev/null
+++ b/tests/fixtures/antipatterns/cssinjs-should-pass.tsx
@@ -0,0 +1,25 @@
+// Clean CSS-in-JS patterns -- no anti-patterns
+
+import styled from 'styled-components';
+import { css } from '@emotion/react';
+
+export const Card = styled.div`
+ border-radius: 12px;
+ padding: 24px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ font-family: 'Geist', system-ui, sans-serif;
+`;
+
+export const Hero = styled.section`
+ background-color: #0f172a;
+ padding: 80px 20px;
+
+ h1 {
+ color: #f8fafc;
+ font-size: 3rem;
+ }
+`;
+
+export const smoothPanel = css`
+ transition: opacity 0.3s ease, transform 0.2s ease;
+`;
diff --git a/tests/fixtures/antipatterns/framework-next-cssinjs/components/FeatureGrid.tsx b/tests/fixtures/antipatterns/framework-next-cssinjs/components/FeatureGrid.tsx
new file mode 100644
index 000000000..7a2bbe084
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-cssinjs/components/FeatureGrid.tsx
@@ -0,0 +1,58 @@
+import styled from "styled-components";
+
+const Grid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 32px;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 80px 24px;
+`;
+
+const Card = styled.div`
+ border-left: 4px solid #8b5cf6;
+ border-radius: 16px;
+ background: #1a1a2e;
+ padding: 32px;
+ box-shadow: 0 0 25px rgba(139, 92, 246, 0.2);
+ transition: width 0.3s ease;
+`;
+
+const CardIcon = styled.div`
+ font-size: 40px;
+ margin-bottom: 16px;
+ animation: bounce 2s infinite;
+`;
+
+const CardTitle = styled.h3`
+ font-size: 20px;
+ font-weight: 700;
+ color: #a855f7;
+ margin-bottom: 8px;
+`;
+
+const CardDescription = styled.p`
+ font-size: 15px;
+ color: #6b7280;
+ line-height: 1.6;
+`;
+
+const features = [
+ { icon: "⚡", title: "Blazing Fast", description: "Optimized for speed with edge-first architecture." },
+ { icon: "🔒", title: "Secure by Default", description: "Enterprise-grade security with zero configuration." },
+ { icon: "📦", title: "Modular Design", description: "Pick and choose only what you need. No bloat." },
+];
+
+export function FeatureGrid() {
+ return (
+
+ {features.map((feature) => (
+
+ {feature.icon}
+ {feature.title}
+ {feature.description}
+
+ ))}
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-cssinjs/components/GlobalStyle.tsx b/tests/fixtures/antipatterns/framework-next-cssinjs/components/GlobalStyle.tsx
new file mode 100644
index 000000000..1bbda6c91
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-cssinjs/components/GlobalStyle.tsx
@@ -0,0 +1,18 @@
+import { createGlobalStyle } from "styled-components";
+
+export const GlobalStyle = createGlobalStyle`
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+
+ body {
+ font-family: 'Inter', sans-serif;
+ background-color: #000000;
+ color: #ffffff;
+ line-height: 1.6;
+ }
+`;
diff --git a/tests/fixtures/antipatterns/framework-next-cssinjs/components/Hero.tsx b/tests/fixtures/antipatterns/framework-next-cssinjs/components/Hero.tsx
new file mode 100644
index 000000000..a11392630
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-cssinjs/components/Hero.tsx
@@ -0,0 +1,61 @@
+import styled from "styled-components";
+
+const Section = styled.section`
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 80px 24px;
+ text-align: center;
+`;
+
+const Title = styled.h1`
+ font-size: 64px;
+ font-weight: 800;
+ font-family: 'Montserrat', sans-serif;
+ background: linear-gradient(135deg, #a855f7, #06b6d4);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ margin-bottom: 24px;
+ line-height: 1.1;
+`;
+
+const Subtitle = styled.p`
+ font-size: 20px;
+ color: #6b7280;
+ max-width: 600px;
+ margin-bottom: 48px;
+`;
+
+const CTAButton = styled.button`
+ padding: 16px 48px;
+ font-size: 18px;
+ font-weight: 600;
+ color: white;
+ background: linear-gradient(135deg, #8b5cf6, #6366f1);
+ border: none;
+ border-radius: 12px;
+ cursor: pointer;
+ box-shadow: 0 0 40px rgba(139, 92, 246, 0.4);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 0 60px rgba(139, 92, 246, 0.6);
+ }
+`;
+
+export function Hero() {
+ return (
+
+ Build the Future
+
+ The most powerful platform for modern web development.
+ Ship faster, scale easier.
+
+ Get Started Free
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-cssinjs/components/Testimonials.tsx b/tests/fixtures/antipatterns/framework-next-cssinjs/components/Testimonials.tsx
new file mode 100644
index 000000000..60a4fd506
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-cssinjs/components/Testimonials.tsx
@@ -0,0 +1,53 @@
+import styled from "styled-components";
+
+const Section = styled.section`
+ padding: 80px 24px;
+ max-width: 800px;
+ margin: 0 auto;
+`;
+
+const SectionTitle = styled.h2`
+ font-size: 36px;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 48px;
+ background: linear-gradient(to right, #a855f7, #ec4899);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+`;
+
+const Quote = styled.blockquote`
+ border-left: 4px solid #6366f1;
+ border-radius: 12px;
+ background: #1a1a2e;
+ padding: 24px 32px;
+ margin-bottom: 24px;
+ font-size: 16px;
+ color: #d1d5db;
+ font-style: italic;
+`;
+
+const Author = styled.p`
+ font-size: 14px;
+ color: #6b7280;
+ margin-top: 12px;
+ font-style: normal;
+`;
+
+export function Testimonials() {
+ return (
+
+ What People Say
+
+ "This platform completely transformed our workflow. Deployment went from
+ hours to minutes."
+ -- Jane Smith, CTO at TechCorp
+
+
+ "The developer experience is unmatched. I can't imagine going back."
+ -- Alex Chen, Senior Engineer
+
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-cssinjs/next.config.js b/tests/fixtures/antipatterns/framework-next-cssinjs/next.config.js
new file mode 100644
index 000000000..7b09dd40f
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-cssinjs/next.config.js
@@ -0,0 +1,8 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ compiler: {
+ styledComponents: true,
+ },
+};
+
+module.exports = nextConfig;
diff --git a/tests/fixtures/antipatterns/framework-next-cssinjs/pages/_app.tsx b/tests/fixtures/antipatterns/framework-next-cssinjs/pages/_app.tsx
new file mode 100644
index 000000000..2e3cb4fc1
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-cssinjs/pages/_app.tsx
@@ -0,0 +1,27 @@
+import type { AppProps } from "next/app";
+import { ThemeProvider } from "styled-components";
+import { GlobalStyle } from "../components/GlobalStyle";
+
+const theme = {
+ colors: {
+ primary: "#8b5cf6",
+ secondary: "#06b6d4",
+ background: "#000000",
+ surface: "#1a1a2e",
+ text: "#ffffff",
+ muted: "#6b7280",
+ },
+ fonts: {
+ body: "'Inter', sans-serif",
+ heading: "'Montserrat', sans-serif",
+ },
+};
+
+export default function App({ Component, pageProps }: AppProps) {
+ return (
+
+
+
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-cssinjs/pages/index.tsx b/tests/fixtures/antipatterns/framework-next-cssinjs/pages/index.tsx
new file mode 100644
index 000000000..ec6721b4d
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-cssinjs/pages/index.tsx
@@ -0,0 +1,13 @@
+import { Hero } from "../components/Hero";
+import { FeatureGrid } from "../components/FeatureGrid";
+import { Testimonials } from "../components/Testimonials";
+
+export default function Home() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/app/globals.css b/tests/fixtures/antipatterns/framework-next-modules/app/globals.css
new file mode 100644
index 000000000..f5804bc9d
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/app/globals.css
@@ -0,0 +1,18 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+body {
+ font-family: 'Roboto', sans-serif;
+ background-color: #000000;
+ color: #fff;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/app/layout.tsx b/tests/fixtures/antipatterns/framework-next-modules/app/layout.tsx
new file mode 100644
index 000000000..fe7b15ec3
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/app/layout.tsx
@@ -0,0 +1,19 @@
+import type { Metadata } from "next";
+import "./globals.css";
+
+export const metadata: Metadata = {
+ title: "Dashboard",
+ description: "Analytics dashboard",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/app/page.module.css b/tests/fixtures/antipatterns/framework-next-modules/app/page.module.css
new file mode 100644
index 000000000..e18b425ba
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/app/page.module.css
@@ -0,0 +1,25 @@
+.container {
+ display: flex;
+ min-height: 100vh;
+}
+
+.main {
+ flex: 1;
+ padding: 32px;
+}
+
+.title {
+ font-size: 28px;
+ font-weight: 700;
+ margin-bottom: 32px;
+ background: linear-gradient(to right, #a855f7, #06b6d4);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 24px;
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/app/page.tsx b/tests/fixtures/antipatterns/framework-next-modules/app/page.tsx
new file mode 100644
index 000000000..c7709ebe8
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/app/page.tsx
@@ -0,0 +1,19 @@
+import { StatsCard } from "../components/StatsCard";
+import { Sidebar } from "../components/Sidebar";
+import styles from "./page.module.css";
+
+export default function Dashboard() {
+ return (
+
+
+
+ Dashboard
+
+
+
+
+
+
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/components/Sidebar.module.css b/tests/fixtures/antipatterns/framework-next-modules/components/Sidebar.module.css
new file mode 100644
index 000000000..4a078635b
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/components/Sidebar.module.css
@@ -0,0 +1,38 @@
+.sidebar {
+ width: 240px;
+ background: #111827;
+ border-right: 3px solid #4f46e5;
+ padding: 24px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+}
+
+.logo {
+ font-size: 20px;
+ font-weight: 700;
+ color: #fff;
+ padding: 0 8px;
+}
+
+.nav {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.navItem {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ color: #9ca3af;
+ font-size: 14px;
+ transition: background 0.2s ease;
+}
+
+.navItem:hover {
+ background: rgba(255, 255, 255, 0.05);
+ color: #fff;
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/components/Sidebar.tsx b/tests/fixtures/antipatterns/framework-next-modules/components/Sidebar.tsx
new file mode 100644
index 000000000..672ecaf04
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/components/Sidebar.tsx
@@ -0,0 +1,24 @@
+import styles from "./Sidebar.module.css";
+
+const navItems = [
+ { label: "Overview", icon: "📊" },
+ { label: "Analytics", icon: "📈" },
+ { label: "Customers", icon: "👥" },
+ { label: "Settings", icon: "⚙️" },
+];
+
+export function Sidebar() {
+ return (
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/components/StatsCard.module.css b/tests/fixtures/antipatterns/framework-next-modules/components/StatsCard.module.css
new file mode 100644
index 000000000..d1d6780c5
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/components/StatsCard.module.css
@@ -0,0 +1,34 @@
+.card {
+ border-left: 4px solid #6366f1;
+ border-radius: 12px;
+ background: #1e1e2e;
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ box-shadow: 0 0 30px rgba(99, 102, 241, 0.3);
+ transition: width 0.3s ease;
+}
+
+.label {
+ font-size: 13px;
+ color: #9ca3af;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.value {
+ font-size: 32px;
+ font-weight: 700;
+ font-family: 'Inter', sans-serif;
+}
+
+.changeUp {
+ color: #22c55e;
+ font-size: 14px;
+}
+
+.changeDown {
+ color: #ef4444;
+ font-size: 14px;
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/components/StatsCard.tsx b/tests/fixtures/antipatterns/framework-next-modules/components/StatsCard.tsx
new file mode 100644
index 000000000..05126fdfb
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/components/StatsCard.tsx
@@ -0,0 +1,21 @@
+import styles from "./StatsCard.module.css";
+
+interface StatsCardProps {
+ label: string;
+ value: string;
+ change: string;
+}
+
+export function StatsCard({ label, value, change }: StatsCardProps) {
+ const isPositive = change.startsWith("+");
+
+ return (
+
+ {label}
+ {value}
+
+ {change}
+
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-modules/next.config.mjs b/tests/fixtures/antipatterns/framework-next-modules/next.config.mjs
new file mode 100644
index 000000000..4678774e6
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-modules/next.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+export default nextConfig;
diff --git a/tests/fixtures/antipatterns/framework-next-tailwind/app/globals.css b/tests/fixtures/antipatterns/framework-next-tailwind/app/globals.css
new file mode 100644
index 000000000..382b44fb8
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-tailwind/app/globals.css
@@ -0,0 +1,20 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(
+ to bottom,
+ transparent,
+ rgb(var(--background-end-rgb))
+ )
+ rgb(var(--background-start-rgb));
+ font-family: 'Inter', sans-serif;
+}
diff --git a/tests/fixtures/antipatterns/framework-next-tailwind/app/layout.tsx b/tests/fixtures/antipatterns/framework-next-tailwind/app/layout.tsx
new file mode 100644
index 000000000..3314e4780
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-tailwind/app/layout.tsx
@@ -0,0 +1,22 @@
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+import "./globals.css";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Create Next App",
+ description: "Generated by create next app",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-tailwind/app/page.tsx b/tests/fixtures/antipatterns/framework-next-tailwind/app/page.tsx
new file mode 100644
index 000000000..cbc5df651
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-tailwind/app/page.tsx
@@ -0,0 +1,50 @@
+import { FeatureCard } from "../components/FeatureCard";
+import { PricingCard } from "../components/PricingCard";
+
+export default function Home() {
+ return (
+
+
+
+ Get started by editing
+ app/page.tsx
+
+
+
+
+
+ Welcome to Our Platform
+
+
+ The next generation of web development
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-tailwind/components/FeatureCard.tsx b/tests/fixtures/antipatterns/framework-next-tailwind/components/FeatureCard.tsx
new file mode 100644
index 000000000..016c28b3f
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-tailwind/components/FeatureCard.tsx
@@ -0,0 +1,15 @@
+interface FeatureCardProps {
+ title: string;
+ description: string;
+ icon: string;
+}
+
+export function FeatureCard({ title, description, icon }: FeatureCardProps) {
+ return (
+
+
{icon}
+
{title}
+
{description}
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-tailwind/components/PricingCard.tsx b/tests/fixtures/antipatterns/framework-next-tailwind/components/PricingCard.tsx
new file mode 100644
index 000000000..25ee66bb5
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-tailwind/components/PricingCard.tsx
@@ -0,0 +1,35 @@
+interface PricingCardProps {
+ name: string;
+ price: string;
+ features: string[];
+ highlighted?: boolean;
+}
+
+export function PricingCard({ name, price, features, highlighted }: PricingCardProps) {
+ return (
+
+
{name}
+
+ {price}
+ /mo
+
+
+ {features.map((feature) => (
+ -
+ ✓
+ {feature}
+
+ ))}
+
+
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-next-tailwind/next.config.mjs b/tests/fixtures/antipatterns/framework-next-tailwind/next.config.mjs
new file mode 100644
index 000000000..4678774e6
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-tailwind/next.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+export default nextConfig;
diff --git a/tests/fixtures/antipatterns/framework-next-tailwind/tailwind.config.ts b/tests/fixtures/antipatterns/framework-next-tailwind/tailwind.config.ts
new file mode 100644
index 000000000..7e4bd91a0
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-next-tailwind/tailwind.config.ts
@@ -0,0 +1,20 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ theme: {
+ extend: {
+ backgroundImage: {
+ "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
+ "gradient-conic":
+ "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
+ },
+ },
+ },
+ plugins: [],
+};
+export default config;
diff --git a/tests/fixtures/antipatterns/framework-vite/main.tsx b/tests/fixtures/antipatterns/framework-vite/main.tsx
new file mode 100644
index 000000000..26f65145e
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-vite/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+export function App() {
+ return (
+
+
Hello Vite
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/framework-vite/vite.config.ts b/tests/fixtures/antipatterns/framework-vite/vite.config.ts
new file mode 100644
index 000000000..eaa85a2e0
--- /dev/null
+++ b/tests/fixtures/antipatterns/framework-vite/vite.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 8080,
+ },
+});
diff --git a/tests/fixtures/antipatterns/jsx-should-flag.jsx b/tests/fixtures/antipatterns/jsx-should-flag.jsx
new file mode 100644
index 000000000..525eef74c
--- /dev/null
+++ b/tests/fixtures/antipatterns/jsx-should-flag.jsx
@@ -0,0 +1,56 @@
+// A typical Next.js/React component with anti-patterns
+
+import React from 'react';
+import { motion } from 'framer-motion';
+
+export function FeatureCard({ title, description, icon }) {
+ return (
+
+ );
+}
+
+export function HeroSection() {
+ return (
+
+
+ Welcome to the Future
+
+ Build something amazing today
+
+ );
+}
+
+export function StatsCard({ value, label }) {
+ return (
+
+ );
+}
+
+export function AnimatedPanel({ children }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/jsx-should-pass.jsx b/tests/fixtures/antipatterns/jsx-should-pass.jsx
new file mode 100644
index 000000000..b58305a81
--- /dev/null
+++ b/tests/fixtures/antipatterns/jsx-should-pass.jsx
@@ -0,0 +1,33 @@
+// Clean React component -- no anti-patterns
+
+import React from 'react';
+
+export function FeatureCard({ title, description, icon }) {
+ return (
+
+ );
+}
+
+export function HeroSection() {
+ return (
+
+ Welcome
+ Build something amazing today
+
+ );
+}
+
+export function StatsCard({ value, label }) {
+ return (
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/multifile/App.tsx b/tests/fixtures/antipatterns/multifile/App.tsx
new file mode 100644
index 000000000..41bdf672c
--- /dev/null
+++ b/tests/fixtures/antipatterns/multifile/App.tsx
@@ -0,0 +1,14 @@
+// App that imports components -- anti-patterns are in the imported files
+import React from 'react';
+import { Card } from './Card';
+import './styles.css';
+
+export function App() {
+ return (
+
+ Dashboard
+
+
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/multifile/Card.tsx b/tests/fixtures/antipatterns/multifile/Card.tsx
new file mode 100644
index 000000000..259ac2212
--- /dev/null
+++ b/tests/fixtures/antipatterns/multifile/Card.tsx
@@ -0,0 +1,16 @@
+// Component with anti-patterns
+import React from 'react';
+
+interface CardProps {
+ title: string;
+ value: string;
+}
+
+export function Card({ title, value }: CardProps) {
+ return (
+
+ );
+}
diff --git a/tests/fixtures/antipatterns/multifile/styles.css b/tests/fixtures/antipatterns/multifile/styles.css
new file mode 100644
index 000000000..cb62d0760
--- /dev/null
+++ b/tests/fixtures/antipatterns/multifile/styles.css
@@ -0,0 +1,9 @@
+/* Global styles with anti-patterns */
+body {
+ font-family: 'Inter', sans-serif;
+ background-color: #000000;
+}
+
+.highlight {
+ animation: bounce 0.5s ease-in-out;
+}
diff --git a/tests/fixtures/antipatterns/multifile/theme.scss b/tests/fixtures/antipatterns/multifile/theme.scss
new file mode 100644
index 000000000..2f7cab2b2
--- /dev/null
+++ b/tests/fixtures/antipatterns/multifile/theme.scss
@@ -0,0 +1,6 @@
+@import './variables';
+
+.card {
+ border-left: 4px solid $primary;
+ border-radius: 12px;
+}
diff --git a/tests/fixtures/antipatterns/multifile/variables.scss b/tests/fixtures/antipatterns/multifile/variables.scss
new file mode 100644
index 000000000..c8c5ae8d7
--- /dev/null
+++ b/tests/fixtures/antipatterns/multifile/variables.scss
@@ -0,0 +1,3 @@
+$primary: #3b82f6;
+$radius: 12px;
+$spacing: 16px;
diff --git a/tests/fixtures/antipatterns/svelte-should-flag.svelte b/tests/fixtures/antipatterns/svelte-should-flag.svelte
new file mode 100644
index 000000000..ade40a091
--- /dev/null
+++ b/tests/fixtures/antipatterns/svelte-should-flag.svelte
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/tests/fixtures/antipatterns/svelte-should-pass.svelte b/tests/fixtures/antipatterns/svelte-should-pass.svelte
new file mode 100644
index 000000000..d009f97aa
--- /dev/null
+++ b/tests/fixtures/antipatterns/svelte-should-pass.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/tests/fixtures/antipatterns/vue-should-flag.vue b/tests/fixtures/antipatterns/vue-should-flag.vue
new file mode 100644
index 000000000..69b20ec34
--- /dev/null
+++ b/tests/fixtures/antipatterns/vue-should-flag.vue
@@ -0,0 +1,36 @@
+
+
+
{{ title }}
+
{{ description }}
+
+
+
+
+
+
+
diff --git a/tests/fixtures/antipatterns/vue-should-pass.vue b/tests/fixtures/antipatterns/vue-should-pass.vue
new file mode 100644
index 000000000..d36be3176
--- /dev/null
+++ b/tests/fixtures/antipatterns/vue-should-pass.vue
@@ -0,0 +1,23 @@
+
+
+
{{ title }}
+
{{ description }}
+
+
+
+
+
+
+