Skip to content

Commit 9ab52bd

Browse files
committed
feat(analyzer): implement project analysis service
- Analyze package.json and detect 50+ frameworks - Detect architecture patterns from directory structure - Parse TypeScript, ESLint, Prettier config files - Sample and categorize source code files - NestJS service integrating all analyzers - Comprehensive test coverage Enables automatic config generation from project analysis. close #21
1 parent c1e8699 commit 9ab52bd

13 files changed

Lines changed: 2182 additions & 0 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Module } from '@nestjs/common';
2+
import { AnalyzerService } from './analyzer.service';
3+
4+
/**
5+
* Module for project analysis functionality
6+
*
7+
* Provides services for analyzing:
8+
* - Package.json (dependencies, frameworks)
9+
* - Directory structure (architecture patterns)
10+
* - Config files (TypeScript, ESLint, Prettier)
11+
* - Source code samples
12+
*/
13+
@Module({
14+
providers: [AnalyzerService],
15+
exports: [AnalyzerService],
16+
})
17+
export class AnalyzerModule {}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { AnalyzerService } from './analyzer.service';
3+
4+
describe('AnalyzerService', () => {
5+
const service = new AnalyzerService();
6+
7+
describe('analyzeProject', () => {
8+
it('should return null packageInfo for non-existent path', async () => {
9+
const result = await service.analyzeProject('/non/existent/path');
10+
11+
expect(result.packageInfo).toBeNull();
12+
expect(result.directoryStructure.rootDirs).toEqual([]);
13+
expect(result.directoryStructure.rootFiles).toEqual([]);
14+
expect(result.codeSamples).toEqual([]);
15+
});
16+
17+
it('should analyze current project', async () => {
18+
// Analyze the mcp-server itself
19+
const result = await service.analyzeProject(process.cwd());
20+
21+
// Should find package.json
22+
expect(result.packageInfo).not.toBeNull();
23+
expect(result.packageInfo?.name).toBeDefined();
24+
25+
// Should detect TypeScript
26+
expect(result.detectedPatterns).toContain('TypeScript');
27+
28+
// Should find some directories
29+
expect(result.directoryStructure.rootDirs).toContain('src');
30+
expect(result.directoryStructure.totalDirs).toBeGreaterThan(0);
31+
expect(result.directoryStructure.totalFiles).toBeGreaterThan(0);
32+
33+
// Should have allFiles populated
34+
expect(result.directoryStructure.allFiles.length).toBe(
35+
result.directoryStructure.totalFiles,
36+
);
37+
});
38+
39+
it('should respect maxCodeSamples option', async () => {
40+
const result = await service.analyzeProject(process.cwd(), {
41+
maxCodeSamples: 2,
42+
});
43+
44+
expect(result.codeSamples.length).toBeLessThanOrEqual(2);
45+
});
46+
47+
it('should detect NestJS framework', async () => {
48+
const result = await service.analyzeProject(process.cwd());
49+
50+
expect(result.packageInfo?.detectedFrameworks).toContainEqual(
51+
expect.objectContaining({ name: 'NestJS', category: 'backend' }),
52+
);
53+
54+
expect(result.detectedPatterns).toContain('NestJS Backend');
55+
});
56+
57+
it('should detect config files', async () => {
58+
const result = await service.analyzeProject(process.cwd());
59+
60+
// Should find tsconfig.json
61+
expect(result.configFiles.typescript).toBeDefined();
62+
expect(result.configFiles.typescript?.path).toBe('tsconfig.json');
63+
});
64+
});
65+
66+
describe('quickAnalyze', () => {
67+
it('should return package info only', async () => {
68+
const result = await service.quickAnalyze(process.cwd());
69+
70+
expect(result).not.toBeNull();
71+
expect(result?.name).toBeDefined();
72+
expect(result?.detectedFrameworks).toBeDefined();
73+
});
74+
75+
it('should return null for non-existent path', async () => {
76+
const result = await service.quickAnalyze('/non/existent/path');
77+
78+
expect(result).toBeNull();
79+
});
80+
});
81+
82+
describe('inferPatterns', () => {
83+
it('should infer patterns from analysis results', async () => {
84+
const result = await service.analyzeProject(process.cwd());
85+
86+
// Should have detected patterns
87+
expect(result.detectedPatterns.length).toBeGreaterThan(0);
88+
89+
// Should include TypeScript since we have it as dependency
90+
expect(result.detectedPatterns).toContain('TypeScript');
91+
});
92+
93+
it('should remove duplicate patterns', async () => {
94+
const result = await service.analyzeProject(process.cwd());
95+
96+
const uniquePatterns = new Set(result.detectedPatterns);
97+
expect(result.detectedPatterns.length).toBe(uniquePatterns.size);
98+
});
99+
});
100+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { Injectable } from '@nestjs/common';
2+
import type { ProjectAnalysis, PackageInfo } from './analyzer.types';
3+
import { analyzePackage } from './package.analyzer';
4+
import { analyzeDirectory } from './directory.analyzer';
5+
import { analyzeConfigs } from './config.analyzer';
6+
import { sampleCode } from './code.sampler';
7+
8+
/**
9+
* Options for project analysis
10+
*/
11+
export interface AnalyzeOptions {
12+
/** Maximum number of code samples to collect */
13+
maxCodeSamples?: number;
14+
/** Custom ignore patterns */
15+
ignorePatterns?: string[];
16+
}
17+
18+
/**
19+
* Default analysis options
20+
*/
21+
const DEFAULT_OPTIONS: Required<AnalyzeOptions> = {
22+
maxCodeSamples: 5,
23+
ignorePatterns: [],
24+
};
25+
26+
/**
27+
* Service for analyzing project structure and configuration
28+
*/
29+
@Injectable()
30+
export class AnalyzerService {
31+
/**
32+
* Perform a complete project analysis
33+
*
34+
* @param projectRoot - Root directory of the project to analyze
35+
* @param options - Analysis options
36+
* @returns Complete project analysis result
37+
*/
38+
async analyzeProject(
39+
projectRoot: string,
40+
options: AnalyzeOptions = {},
41+
): Promise<ProjectAnalysis> {
42+
const opts = { ...DEFAULT_OPTIONS, ...options };
43+
44+
// Run all analyzers in parallel
45+
const [packageInfo, dirAnalysis] = await Promise.all([
46+
analyzePackage(projectRoot),
47+
analyzeDirectory(projectRoot, opts.ignorePatterns),
48+
]);
49+
50+
// Config analysis depends on directory analysis for root files
51+
const configFiles = await analyzeConfigs(projectRoot, dirAnalysis.rootFiles);
52+
53+
// Sample code files using all scanned files
54+
const codeSamples = await sampleCode(projectRoot, dirAnalysis.allFiles, opts.maxCodeSamples);
55+
56+
// Infer patterns from all collected data
57+
const detectedPatterns = this.inferPatterns(packageInfo, dirAnalysis);
58+
59+
return {
60+
packageInfo,
61+
directoryStructure: dirAnalysis,
62+
configFiles,
63+
codeSamples,
64+
detectedPatterns,
65+
};
66+
}
67+
68+
/**
69+
* Infer high-level patterns from analysis results
70+
*/
71+
private inferPatterns(
72+
packageInfo: PackageInfo | null,
73+
dirAnalysis: { patterns: { name: string; confidence: number }[] },
74+
): string[] {
75+
const patterns: string[] = [];
76+
77+
// Add architecture patterns with high confidence
78+
for (const pattern of dirAnalysis.patterns) {
79+
if (pattern.confidence >= 0.5) {
80+
patterns.push(pattern.name);
81+
}
82+
}
83+
84+
// Add framework patterns from package.json
85+
if (packageInfo) {
86+
for (const framework of packageInfo.detectedFrameworks) {
87+
if (framework.category === 'frontend' || framework.category === 'fullstack') {
88+
patterns.push(`${framework.name} Project`);
89+
}
90+
if (framework.category === 'backend') {
91+
patterns.push(`${framework.name} Backend`);
92+
}
93+
}
94+
95+
// Detect TypeScript project
96+
if (
97+
packageInfo.devDependencies['typescript'] ||
98+
packageInfo.dependencies['typescript']
99+
) {
100+
patterns.push('TypeScript');
101+
}
102+
103+
// Detect monorepo patterns
104+
if (
105+
packageInfo.devDependencies['lerna'] ||
106+
packageInfo.devDependencies['turbo'] ||
107+
packageInfo.devDependencies['nx']
108+
) {
109+
patterns.push('Monorepo');
110+
}
111+
}
112+
113+
// Remove duplicates
114+
return [...new Set(patterns)];
115+
}
116+
117+
/**
118+
* Quick analysis - package.json only
119+
*/
120+
async quickAnalyze(projectRoot: string): Promise<PackageInfo | null> {
121+
return analyzePackage(projectRoot);
122+
}
123+
}

0 commit comments

Comments
 (0)