Init
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* Python Framework Resolver
|
||||
*
|
||||
* Handles Django, Flask, and FastAPI patterns.
|
||||
*/
|
||||
|
||||
import { Node } from '../../types';
|
||||
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
|
||||
|
||||
export const djangoResolver: FrameworkResolver = {
|
||||
name: 'django',
|
||||
|
||||
detect(context: ResolutionContext): boolean {
|
||||
// Check for Django in requirements.txt or setup.py
|
||||
const requirements = context.readFile('requirements.txt');
|
||||
if (requirements && requirements.includes('django')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const setup = context.readFile('setup.py');
|
||||
if (setup && setup.includes('django')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pyproject = context.readFile('pyproject.toml');
|
||||
if (pyproject && pyproject.includes('django')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for manage.py (Django signature)
|
||||
return context.fileExists('manage.py');
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
// Pattern 1: Model references
|
||||
if (ref.referenceName.endsWith('Model') || /^[A-Z][a-z]+$/.test(ref.referenceName)) {
|
||||
const result = resolveModel(ref.referenceName, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: result,
|
||||
confidence: 0.8,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: View references
|
||||
if (ref.referenceName.endsWith('View') || ref.referenceName.endsWith('ViewSet')) {
|
||||
const result = resolveView(ref.referenceName, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: result,
|
||||
confidence: 0.8,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: Form references
|
||||
if (ref.referenceName.endsWith('Form')) {
|
||||
const result = resolveForm(ref.referenceName, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: result,
|
||||
confidence: 0.8,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
extractNodes(filePath: string, content: string): Node[] {
|
||||
const nodes: Node[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
// Extract URL patterns
|
||||
// path('route/', view, name='name')
|
||||
const urlPatterns = [
|
||||
/path\s*\(\s*['"]([^'"]+)['"],\s*(\w+)/g,
|
||||
/url\s*\(\s*r?['"]([^'"]+)['"],\s*(\w+)/g,
|
||||
];
|
||||
|
||||
for (const pattern of urlPatterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
const [, urlPath] = match;
|
||||
const line = content.slice(0, match.index).split('\n').length;
|
||||
|
||||
nodes.push({
|
||||
id: `route:${filePath}:${urlPath}:${line}`,
|
||||
kind: 'route',
|
||||
name: urlPath!,
|
||||
qualifiedName: `${filePath}::route:${urlPath}`,
|
||||
filePath,
|
||||
startLine: line,
|
||||
endLine: line,
|
||||
startColumn: 0,
|
||||
endColumn: match[0].length,
|
||||
language: 'python',
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
},
|
||||
};
|
||||
|
||||
export const flaskResolver: FrameworkResolver = {
|
||||
name: 'flask',
|
||||
|
||||
detect(context: ResolutionContext): boolean {
|
||||
const requirements = context.readFile('requirements.txt');
|
||||
if (requirements && (requirements.includes('flask') || requirements.includes('Flask'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pyproject = context.readFile('pyproject.toml');
|
||||
if (pyproject && pyproject.includes('flask')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Flask app pattern in common files
|
||||
const appFiles = ['app.py', 'application.py', 'main.py', '__init__.py'];
|
||||
for (const file of appFiles) {
|
||||
const content = context.readFile(file);
|
||||
if (content && content.includes('Flask(__name__)')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
// Pattern 1: Blueprint references
|
||||
if (ref.referenceName.endsWith('_bp') || ref.referenceName.endsWith('_blueprint')) {
|
||||
const result = resolveBlueprint(ref.referenceName, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: result,
|
||||
confidence: 0.8,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
extractNodes(filePath: string, content: string): Node[] {
|
||||
const nodes: Node[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
// Extract Flask route decorators
|
||||
// @app.route('/path') or @blueprint.route('/path')
|
||||
const routePattern = /@(\w+)\.route\s*\(\s*['"]([^'"]+)['"]/g;
|
||||
|
||||
let match;
|
||||
while ((match = routePattern.exec(content)) !== null) {
|
||||
const [, _appOrBp, routePath] = match;
|
||||
const line = content.slice(0, match.index).split('\n').length;
|
||||
|
||||
nodes.push({
|
||||
id: `route:${filePath}:${routePath}:${line}`,
|
||||
kind: 'route',
|
||||
name: `${routePath}`,
|
||||
qualifiedName: `${filePath}::route:${routePath}`,
|
||||
filePath,
|
||||
startLine: line,
|
||||
endLine: line,
|
||||
startColumn: 0,
|
||||
endColumn: match[0].length,
|
||||
language: 'python',
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
},
|
||||
};
|
||||
|
||||
export const fastapiResolver: FrameworkResolver = {
|
||||
name: 'fastapi',
|
||||
|
||||
detect(context: ResolutionContext): boolean {
|
||||
const requirements = context.readFile('requirements.txt');
|
||||
if (requirements && requirements.includes('fastapi')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pyproject = context.readFile('pyproject.toml');
|
||||
if (pyproject && pyproject.includes('fastapi')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for FastAPI app pattern
|
||||
const appFiles = ['app.py', 'main.py', 'api.py'];
|
||||
for (const file of appFiles) {
|
||||
const content = context.readFile(file);
|
||||
if (content && content.includes('FastAPI()')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
// Pattern 1: Router references
|
||||
if (ref.referenceName.endsWith('_router') || ref.referenceName === 'router') {
|
||||
const result = resolveRouter(ref.referenceName, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: result,
|
||||
confidence: 0.8,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Dependency references
|
||||
if (ref.referenceName.startsWith('get_') || ref.referenceName.startsWith('Depends')) {
|
||||
const result = resolveDependency(ref.referenceName, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: result,
|
||||
confidence: 0.75,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
extractNodes(filePath: string, content: string): Node[] {
|
||||
const nodes: Node[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
// Extract FastAPI route decorators
|
||||
// @app.get('/path') or @router.post('/path')
|
||||
const routePattern = /@(\w+)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"]([^'"]+)['"]/g;
|
||||
|
||||
let match;
|
||||
while ((match = routePattern.exec(content)) !== null) {
|
||||
const [, _appOrRouter, method, routePath] = match;
|
||||
const line = content.slice(0, match.index).split('\n').length;
|
||||
|
||||
nodes.push({
|
||||
id: `route:${filePath}:${method!.toUpperCase()}:${routePath}:${line}`,
|
||||
kind: 'route',
|
||||
name: `${method!.toUpperCase()} ${routePath}`,
|
||||
qualifiedName: `${filePath}::${method!.toUpperCase()}:${routePath}`,
|
||||
filePath,
|
||||
startLine: line,
|
||||
endLine: line,
|
||||
startColumn: 0,
|
||||
endColumn: match[0].length,
|
||||
language: 'python',
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
},
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
|
||||
function resolveModel(name: string, context: ResolutionContext): string | null {
|
||||
const modelDirs = ['models', 'app/models', 'src/models'];
|
||||
|
||||
for (const dir of modelDirs) {
|
||||
const allFiles = context.getAllFiles();
|
||||
for (const file of allFiles) {
|
||||
if (file.startsWith(dir) && file.endsWith('.py')) {
|
||||
const nodes = context.getNodesInFile(file);
|
||||
const modelNode = nodes.find(
|
||||
(n) => n.kind === 'class' && n.name === name
|
||||
);
|
||||
if (modelNode) {
|
||||
return modelNode.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveView(name: string, context: ResolutionContext): string | null {
|
||||
const viewDirs = ['views', 'app/views', 'src/views', 'api/views'];
|
||||
|
||||
for (const dir of viewDirs) {
|
||||
const allFiles = context.getAllFiles();
|
||||
for (const file of allFiles) {
|
||||
if (file.startsWith(dir) && file.endsWith('.py')) {
|
||||
const nodes = context.getNodesInFile(file);
|
||||
const viewNode = nodes.find(
|
||||
(n) => (n.kind === 'class' || n.kind === 'function') && n.name === name
|
||||
);
|
||||
if (viewNode) {
|
||||
return viewNode.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveForm(name: string, context: ResolutionContext): string | null {
|
||||
const formDirs = ['forms', 'app/forms', 'src/forms'];
|
||||
|
||||
for (const dir of formDirs) {
|
||||
const allFiles = context.getAllFiles();
|
||||
for (const file of allFiles) {
|
||||
if (file.startsWith(dir) && file.endsWith('.py')) {
|
||||
const nodes = context.getNodesInFile(file);
|
||||
const formNode = nodes.find(
|
||||
(n) => n.kind === 'class' && n.name === name
|
||||
);
|
||||
if (formNode) {
|
||||
return formNode.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveBlueprint(name: string, context: ResolutionContext): string | null {
|
||||
const allFiles = context.getAllFiles();
|
||||
for (const file of allFiles) {
|
||||
if (file.endsWith('.py')) {
|
||||
const nodes = context.getNodesInFile(file);
|
||||
const bpNode = nodes.find(
|
||||
(n) => n.kind === 'variable' && n.name === name
|
||||
);
|
||||
if (bpNode) {
|
||||
return bpNode.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveRouter(name: string, context: ResolutionContext): string | null {
|
||||
const routerDirs = ['routers', 'api', 'routes', 'endpoints'];
|
||||
|
||||
for (const dir of routerDirs) {
|
||||
const allFiles = context.getAllFiles();
|
||||
for (const file of allFiles) {
|
||||
if ((file.startsWith(dir) || file.includes('/routers/')) && file.endsWith('.py')) {
|
||||
const nodes = context.getNodesInFile(file);
|
||||
const routerNode = nodes.find(
|
||||
(n) => n.kind === 'variable' && n.name === name
|
||||
);
|
||||
if (routerNode) {
|
||||
return routerNode.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveDependency(name: string, context: ResolutionContext): string | null {
|
||||
const depDirs = ['dependencies', 'deps', 'core'];
|
||||
|
||||
for (const dir of depDirs) {
|
||||
const allFiles = context.getAllFiles();
|
||||
for (const file of allFiles) {
|
||||
if ((file.startsWith(dir) || file.includes('/dependencies/')) && file.endsWith('.py')) {
|
||||
const nodes = context.getNodesInFile(file);
|
||||
const depNode = nodes.find(
|
||||
(n) => n.kind === 'function' && n.name === name
|
||||
);
|
||||
if (depNode) {
|
||||
return depNode.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user