This commit is contained in:
251
app/src/analyzers/conventionExtractor.ts
Normal file
251
app/src/analyzers/conventionExtractor.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { CodingConventions, FileNamingConvention } from '../types';
|
||||||
|
import { readFileContent, globFiles } from '../utils/fileUtils';
|
||||||
|
|
||||||
|
const TEST_FRAMEWORKS: Record<string, string> = {
|
||||||
|
jest: 'jest',
|
||||||
|
'jest-environment-jsdom': 'jest',
|
||||||
|
vitest: 'vitest',
|
||||||
|
mocha: 'mocha',
|
||||||
|
chai: 'mocha',
|
||||||
|
pytest: 'pytest',
|
||||||
|
'pytest-cov': 'pytest',
|
||||||
|
unittest: 'unittest',
|
||||||
|
nose: 'nose',
|
||||||
|
'testing-library/jest-dom': 'jest',
|
||||||
|
'testing-library/react': 'jest',
|
||||||
|
'@testing-library/jest-dom': 'jest',
|
||||||
|
'@testing-library/react': 'jest'
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function extractConventions(
|
||||||
|
directory: string,
|
||||||
|
projectType: string
|
||||||
|
): Promise<CodingConventions> {
|
||||||
|
const files = await globFiles(
|
||||||
|
['**/*.{ts,js,tsx,jsx,py,go,rs,java}'],
|
||||||
|
directory,
|
||||||
|
['node_modules/**', 'dist/**', 'build/**', '.git/**']
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileNamingConventions = detectFileNamingConvention(files);
|
||||||
|
const importStyle = detectImportStyle(files);
|
||||||
|
const testingFramework = detectTestingFramework(directory, files);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileNamingConvention: fileNamingConventions,
|
||||||
|
importStyle,
|
||||||
|
testingFramework: testingFramework?.framework,
|
||||||
|
testingStyle: testingFramework?.style || null,
|
||||||
|
componentStyle: detectComponentStyle(files, projectType),
|
||||||
|
modulePattern: detectModulePattern(files, projectType)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFileNamingConvention(files: string[]): FileNamingConvention {
|
||||||
|
const sampleSize = Math.min(files.length, 50);
|
||||||
|
const samples = files.slice(0, sampleSize);
|
||||||
|
|
||||||
|
const scores: Record<string, number> = {
|
||||||
|
camelCase: 0,
|
||||||
|
snake_case: 0,
|
||||||
|
PascalCase: 0,
|
||||||
|
'kebab-case': 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const file of samples) {
|
||||||
|
const filename = path.basename(file, path.extname(file));
|
||||||
|
|
||||||
|
if (/^[a-z]+([A-Z][a-z0-9]*)*$/.test(filename)) {
|
||||||
|
scores.camelCase++;
|
||||||
|
} else if (/^[a-z]+(_[a-z0-9]+)*$/.test(filename)) {
|
||||||
|
scores.snake_case++;
|
||||||
|
} else if (/^[A-Z][a-zA-Z0-9]*$/.test(filename)) {
|
||||||
|
scores.PascalCase++;
|
||||||
|
} else if (/^[a-z]+(-[a-z0-9]+)*$/.test(filename)) {
|
||||||
|
scores['kebab-case']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxScore = Math.max(...Object.values(scores));
|
||||||
|
|
||||||
|
if (maxScore === 0) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
const bestMatch = Object.entries(scores).find(([_, score]) => score === maxScore);
|
||||||
|
return (bestMatch?.[0] as FileNamingConvention) || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectImportStyle(files: string[]): CodingConventions['importStyle'] {
|
||||||
|
const sampleSize = Math.min(files.length, 30);
|
||||||
|
const samples = files.slice(0, sampleSize);
|
||||||
|
|
||||||
|
let es6Imports = 0;
|
||||||
|
let commonjsImports = 0;
|
||||||
|
let relativeImports = 0;
|
||||||
|
let pythonImports = 0;
|
||||||
|
let goImports = 0;
|
||||||
|
|
||||||
|
for (const file of samples) {
|
||||||
|
const content = readFileContent(file);
|
||||||
|
if (!content) continue;
|
||||||
|
|
||||||
|
const ext = path.extname(file);
|
||||||
|
|
||||||
|
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
||||||
|
if (/\bimport\s+.*\s+from\s+['"]/.test(content)) {
|
||||||
|
es6Imports++;
|
||||||
|
}
|
||||||
|
if (/\brequire\s*\(\s*['"]/.test(content)) {
|
||||||
|
commonjsImports++;
|
||||||
|
}
|
||||||
|
if (/from\s+['"]\.?[\/'"].test(content)) {
|
||||||
|
relativeImports++;
|
||||||
|
}
|
||||||
|
} else if (ext === '.py') {
|
||||||
|
if (/\bimport\s+\w+/.test(content) || /\bfrom\s+\w+/.test(content)) {
|
||||||
|
pythonImports++;
|
||||||
|
}
|
||||||
|
} else if (ext === '.go') {
|
||||||
|
if (/\bimport\s*\(/m.test(content) || /\bimport\s+["']/.test(content)) {
|
||||||
|
goImports++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pythonImports > 0) {
|
||||||
|
return 'python';
|
||||||
|
} else if (goImports > 0) {
|
||||||
|
return 'go';
|
||||||
|
} else if (es6Imports > commonjsImports && es6Imports > relativeImports) {
|
||||||
|
return 'es6';
|
||||||
|
} else if (commonjsImports > relativeImports) {
|
||||||
|
return 'commonjs';
|
||||||
|
} else if (relativeImports > es6Imports + commonjsImports) {
|
||||||
|
return 'relative';
|
||||||
|
} else if (es6Imports > 0 || commonjsImports > 0) {
|
||||||
|
return 'mixed';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'es6';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectTestingFramework(
|
||||||
|
directory: string,
|
||||||
|
files: string[]
|
||||||
|
): { framework: string; style: CodingConventions['testingStyle'] } | null {
|
||||||
|
const packageJsonPath = path.join(directory, 'package.json');
|
||||||
|
|
||||||
|
const packageJson = fs.existsSync(packageJsonPath)
|
||||||
|
? JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const allDeps = {
|
||||||
|
...(packageJson?.dependencies || {}),
|
||||||
|
...(packageJson?.devDependencies || {})
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [pkg, framework] of Object.entries(TEST_FRAMEWORKS)) {
|
||||||
|
if (allDeps[pkg]) {
|
||||||
|
const styleMap: Record<string, CodingConventions['testingStyle']> = {
|
||||||
|
jest: 'jest',
|
||||||
|
vitest: 'vitest',
|
||||||
|
mocha: 'mocha',
|
||||||
|
pytest: 'pytest',
|
||||||
|
unittest: 'unittest',
|
||||||
|
testing: 'testing'
|
||||||
|
};
|
||||||
|
return { framework: pkg, style: styleMap[framework] || null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const basename = path.basename(file);
|
||||||
|
|
||||||
|
if (basename.includes('.test.') || basename.includes('.spec.')) {
|
||||||
|
return { framework: 'test runner', style: 'jest' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basename.startsWith('test_') || basename.endsWith('_test.py')) {
|
||||||
|
return { framework: 'pytest', style: 'pytest' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectComponentStyle(files: string[], _projectType: string): CodingConventions['componentStyle'] {
|
||||||
|
const tsxFiles = files.filter(f => f.endsWith('.tsx'));
|
||||||
|
|
||||||
|
if (tsxFiles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleSize = Math.min(tsxFiles.length, 20);
|
||||||
|
const samples = tsxFiles.slice(0, sampleSize);
|
||||||
|
|
||||||
|
let functional = 0;
|
||||||
|
let classBased = 0;
|
||||||
|
let hooks = 0;
|
||||||
|
|
||||||
|
for (const file of samples) {
|
||||||
|
const content = readFileContent(file);
|
||||||
|
if (!content) continue;
|
||||||
|
|
||||||
|
if (/\bfunction\s+\w+\s*\(/.test(content)) {
|
||||||
|
functional++;
|
||||||
|
}
|
||||||
|
if (/\bclass\s+\w+\s+extends\s+(React\.)?Component/.test(content)) {
|
||||||
|
classBased++;
|
||||||
|
}
|
||||||
|
if (/\buse\w+\s*\(/.test(content)) {
|
||||||
|
hooks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hooks > functional + classBased) {
|
||||||
|
return 'hooks';
|
||||||
|
} else if (functional > classBased) {
|
||||||
|
return 'functional';
|
||||||
|
} else if (classBased > 0) {
|
||||||
|
return 'class';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'functional';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectModulePattern(files: string[], _projectType: string): CodingConventions['modulePattern'] {
|
||||||
|
const sampleSize = Math.min(files.length, 30);
|
||||||
|
const samples = files.slice(0, sampleSize);
|
||||||
|
|
||||||
|
let defaultExports = 0;
|
||||||
|
let namedExports = 0;
|
||||||
|
let wildcardImports = 0;
|
||||||
|
|
||||||
|
for (const file of samples) {
|
||||||
|
const content = readFileContent(file);
|
||||||
|
if (!content) continue;
|
||||||
|
|
||||||
|
if (/export\s+default/.test(content)) {
|
||||||
|
defaultExports++;
|
||||||
|
}
|
||||||
|
if (/export\s+{/.test(content)) {
|
||||||
|
namedExports++;
|
||||||
|
}
|
||||||
|
if (/import\s+\*\s+as/.test(content)) {
|
||||||
|
wildcardImports++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultExports > namedExports && defaultExports > wildcardImports) {
|
||||||
|
return 'default';
|
||||||
|
} else if (namedExports > defaultExports && namedExports > wildcardImports) {
|
||||||
|
return 'named';
|
||||||
|
} else if (wildcardImports > defaultExports + namedExports) {
|
||||||
|
return 'wildcard';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user