// Copyright 2025 Chris Bunting
// Brief: Structure Analyzer for Code Complexity Analyzer MCP Server
// Scope: Analyzes code structure including design patterns, anti-patterns, and code duplication
import { ASTNode } from './ASTProcessor.js';
import { Logger } from '../utils/Logger.js';
import { Language } from '@mcp-code-analysis/shared-types';
export interface DesignPattern {
name: string;
type: 'creational' | 'structural' | 'behavioral';
confidence: number;
location: { line: number; column: number };
description: string;
elements: string[];
}
export interface AntiPattern {
name: string;
severity: 'low' | 'medium' | 'high' | 'critical';
confidence: number;
location: { line: number; column: number };
description: string;
suggestion: string;
}
export interface CodeDuplication {
type: 'exact' | 'similar';
lines: number;
locations: Array<{ filePath: string; startLine: number; endLine: number }>;
similarity: number;
}
export interface StructureAnalysisResult {
filePath: string;
language: Language;
designPatterns: DesignPattern[];
antiPatterns: AntiPattern[];
duplications: CodeDuplication[];
inheritanceHierarchy: InheritanceNode[];
interfaces: InterfaceInfo[];
analysisTime: number;
errors: string[];
}
export interface InheritanceNode {
name: string;
type: 'class' | 'interface';
parent?: string;
children: string[];
methods: string[];
properties: string[];
location: { line: number; column: number };
}
export interface InterfaceInfo {
name: string;
methods: string[];
properties: string[];
implementors: string[];
location: { line: number; column: number };
}
export class StructureAnalyzer {
private logger: Logger;
constructor() {
this.logger = new Logger();
}
public async analyzeStructure(
ast: ASTNode,
filePath: string,
language: Language,
sourceCode?: string
): Promise<StructureAnalysisResult> {
const startTime = Date.now();
const errors: string[] = [];
try {
// Analyze design patterns
const designPatterns = this.detectDesignPatterns(ast, language);
// Detect anti-patterns
const antiPatterns = this.detectAntiPatterns(ast, language, sourceCode || '');
// Analyze code duplication (requires source code)
const duplications = sourceCode ? this.analyzeCodeDuplication(sourceCode, filePath) : [];
// Build inheritance hierarchy
const inheritanceHierarchy = this.buildInheritanceHierarchy(ast, language);
// Analyze interfaces
const interfaces = this.analyzeInterfaces(ast, language);
const analysisTime = Date.now() - startTime;
this.logger.info(`Successfully analyzed structure for: ${filePath}`, {
analysisTime,
designPatterns: designPatterns.length,
antiPatterns: antiPatterns.length,
duplications: duplications.length
});
return {
filePath,
language,
designPatterns,
antiPatterns,
duplications,
inheritanceHierarchy,
interfaces,
analysisTime,
errors
};
} catch (error) {
const analysisTime = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
errors.push(errorMessage);
this.logger.error(`Failed to analyze structure for: ${filePath}`, { error: errorMessage });
return {
filePath,
language,
designPatterns: [],
antiPatterns: [],
duplications: [],
inheritanceHierarchy: [],
interfaces: [],
analysisTime,
errors
};
}
}
private detectDesignPatterns(ast: ASTNode, language: Language): DesignPattern[] {
const patterns: DesignPattern[] = [];
// Singleton Pattern
const singletons = this.detectSingletonPattern(ast, language);
patterns.push(...singletons);
// Factory Pattern
const factories = this.detectFactoryPattern(ast, language);
patterns.push(...factories);
// Observer Pattern
const observers = this.detectObserverPattern(ast, language);
patterns.push(...observers);
// Strategy Pattern
const strategies = this.detectStrategyPattern(ast, language);
patterns.push(...strategies);
// Decorator Pattern
const decorators = this.detectDecoratorPattern(ast, language);
patterns.push(...decorators);
return patterns;
}
private detectSingletonPattern(ast: ASTNode, language: Language): DesignPattern[] {
const singletons: DesignPattern[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class') {
const className = node.name;
if (!className) return;
// Look for singleton characteristics
const hasPrivateConstructor = this.hasPrivateConstructor(node, language);
const hasStaticInstance = this.hasStaticInstance(node, language);
const hasGetInstanceMethod = this.hasGetInstanceMethod(node, language);
if (hasPrivateConstructor && (hasStaticInstance || hasGetInstanceMethod)) {
singletons.push({
name: 'Singleton',
type: 'creational',
confidence: 0.8,
location: { line: node.start.line, column: node.start.column },
description: `Class ${className} appears to implement Singleton pattern`,
elements: [className]
});
}
}
});
return singletons;
}
private detectFactoryPattern(ast: ASTNode, language: Language): DesignPattern[] {
const factories: DesignPattern[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class') {
const className = node.name;
if (!className) return;
// Look for factory characteristics
const hasCreateMethods = this.hasCreateMethods(node, language);
const hasFactoryInName = className.toLowerCase().includes('factory');
if (hasCreateMethods || hasFactoryInName) {
factories.push({
name: 'Factory',
type: 'creational',
confidence: hasFactoryInName ? 0.9 : 0.7,
location: { line: node.start.line, column: node.start.column },
description: `Class ${className} appears to implement Factory pattern`,
elements: [className]
});
}
}
});
return factories;
}
private detectObserverPattern(ast: ASTNode, language: Language): DesignPattern[] {
const observers: DesignPattern[] = [];
const subjects: string[] = [];
const observersList: string[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class') {
const className = node.name;
if (!className) return;
const hasAttachMethods = this.hasAttachMethods(node, language);
const hasNotifyMethods = this.hasNotifyMethods(node, language);
const hasObserverInName = className.toLowerCase().includes('observer');
if (hasAttachMethods && hasNotifyMethods) {
subjects.push(className);
}
if (hasObserverInName || this.hasUpdateMethods(node, language)) {
observersList.push(className);
}
}
});
// If we have both subjects and observers, it's likely the Observer pattern
if (subjects.length > 0 && observersList.length > 0) {
observers.push({
name: 'Observer',
type: 'behavioral',
confidence: 0.8,
location: { line: 1, column: 0 },
description: 'Observer pattern detected with subjects and observers',
elements: [...subjects, ...observersList]
});
}
return observers;
}
private detectStrategyPattern(ast: ASTNode, language: Language): DesignPattern[] {
const strategies: DesignPattern[] = [];
const strategyClasses: string[] = [];
const contextClasses: string[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class') {
const className = node.name;
if (!className) return;
const hasStrategyInName = className.toLowerCase().includes('strategy');
const hasContextInName = className.toLowerCase().includes('context');
const hasAlgorithmMethods = this.hasAlgorithmMethods(node, language);
if (hasStrategyInName || hasAlgorithmMethods) {
strategyClasses.push(className);
}
if (hasContextInName) {
contextClasses.push(className);
}
}
});
if (strategyClasses.length > 0 && contextClasses.length > 0) {
strategies.push({
name: 'Strategy',
type: 'behavioral',
confidence: 0.8,
location: { line: 1, column: 0 },
description: 'Strategy pattern detected with strategy and context classes',
elements: [...strategyClasses, ...contextClasses]
});
}
return strategies;
}
private detectDecoratorPattern(ast: ASTNode, language: Language): DesignPattern[] {
const decorators: DesignPattern[] = [];
const decoratorClasses: string[] = [];
const componentClasses: string[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class') {
const className = node.name;
if (!className) return;
const hasDecoratorInName = className.toLowerCase().includes('decorator');
const hasComponentInName = className.toLowerCase().includes('component');
const wrapsOtherClasses = this.wrapsOtherClasses(node, language);
if (hasDecoratorInName || wrapsOtherClasses) {
decoratorClasses.push(className);
}
if (hasComponentInName) {
componentClasses.push(className);
}
}
});
if (decoratorClasses.length > 0 && componentClasses.length > 0) {
decorators.push({
name: 'Decorator',
type: 'structural',
confidence: 0.8,
location: { line: 1, column: 0 },
description: 'Decorator pattern detected with decorator and component classes',
elements: [...decoratorClasses, ...componentClasses]
});
}
return decorators;
}
private detectAntiPatterns(ast: ASTNode, language: Language, sourceCode: string): AntiPattern[] {
const antiPatterns: AntiPattern[] = [];
// God Object anti-pattern
const godObjects = this.detectGodObject(ast, language);
antiPatterns.push(...godObjects);
// Long Method anti-pattern
const longMethods = this.detectLongMethod(ast, language);
antiPatterns.push(...longMethods);
// Large Class anti-pattern
const largeClasses = this.detectLargeClass(ast, language);
antiPatterns.push(...largeClasses);
// Duplicated Code anti-pattern
const duplicatedCode = this.detectDuplicatedCode(sourceCode);
antiPatterns.push(...duplicatedCode);
// Magic Numbers anti-pattern
const magicNumbers = this.detectMagicNumbers(ast, sourceCode);
antiPatterns.push(...magicNumbers);
// Complex Conditional anti-pattern
const complexConditionals = this.detectComplexConditional(ast, language);
antiPatterns.push(...complexConditionals);
return antiPatterns;
}
private detectGodObject(ast: ASTNode, _language: Language): AntiPattern[] {
const godObjects: AntiPattern[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class') {
const className = node.name;
if (!className) return;
const methods = this.findFunctions(node);
const properties = this.findProperties(node);
// God object: too many responsibilities
if (methods.length > 15 || properties.length > 20) {
godObjects.push({
name: 'God Object',
severity: 'high',
confidence: 0.9,
location: { line: node.start.line, column: node.start.column },
description: `Class ${className} has too many responsibilities (${methods.length} methods, ${properties.length} properties)`,
suggestion: 'Consider splitting this class into smaller, more focused classes'
});
}
}
});
return godObjects;
}
private detectLongMethod(ast: ASTNode, _language: Language): AntiPattern[] {
const longMethods: AntiPattern[] = [];
this.traverseAST(ast, (node) => {
if ((node.type === 'function' || node.type === 'method') && node.name) {
const lines = node.end.line - node.start.line + 1;
// Long method: more than 20 lines
if (lines > 20) {
longMethods.push({
name: 'Long Method',
severity: 'medium',
confidence: 0.8,
location: { line: node.start.line, column: node.start.column },
description: `Method ${node.name} is too long (${lines} lines)`,
suggestion: 'Consider breaking this method into smaller, more focused methods'
});
}
}
});
return longMethods;
}
private detectLargeClass(ast: ASTNode, _language: Language): AntiPattern[] {
const largeClasses: AntiPattern[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class') {
const className = node.name;
if (!className) return;
const lines = node.end.line - node.start.line + 1;
// Large class: more than 300 lines
if (lines > 300) {
largeClasses.push({
name: 'Large Class',
severity: 'medium',
confidence: 0.8,
location: { line: node.start.line, column: node.start.column },
description: `Class ${className} is too large (${lines} lines)`,
suggestion: 'Consider splitting this class into smaller, more focused classes'
});
}
}
});
return largeClasses;
}
private detectDuplicatedCode(sourceCode: string): AntiPattern[] {
const duplicatedCode: AntiPattern[] = [];
const lines = sourceCode.split('\n');
// Simple duplication detection
for (let i = 0; i < lines.length - 5; i++) {
const block = lines.slice(i, i + 5).join('\n');
// Look for the same block elsewhere
for (let j = i + 5; j < lines.length - 5; j++) {
const otherBlock = lines.slice(j, j + 5).join('\n');
if (block === otherBlock && block.trim().length > 0) {
duplicatedCode.push({
name: 'Duplicated Code',
severity: 'medium',
confidence: 0.7,
location: { line: i + 1, column: 0 },
description: `Code duplication detected between lines ${i + 1}-${i + 5} and ${j + 1}-${j + 5}`,
suggestion: 'Consider extracting the duplicated code into a separate method'
});
}
}
}
return duplicatedCode;
}
private detectMagicNumbers(_ast: ASTNode, sourceCode: string): AntiPattern[] {
const magicNumbers: AntiPattern[] = [];
const lines = sourceCode.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Look for magic numbers (numbers that aren't 0, 1, or 2)
const numbers = line.match(/\b[3-9]\d*\b/g);
if (numbers) {
for (const number of numbers) {
// Exclude common cases that aren't magic numbers
if (!line.includes(`const ${number}`) &&
!line.includes(`#define ${number}`) &&
!line.includes('array') &&
!line.includes('size') &&
!line.includes('length')) {
magicNumbers.push({
name: 'Magic Number',
severity: 'low',
confidence: 0.6,
location: { line: i + 1, column: 0 },
description: `Magic number ${number} detected`,
suggestion: 'Consider replacing magic numbers with named constants'
});
}
}
}
}
return magicNumbers;
}
private detectComplexConditional(ast: ASTNode, _language: Language): AntiPattern[] {
const complexConditionals: AntiPattern[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'IfStatement' || node.type === 'ConditionalExpression') {
// Look for complex conditions (simplified)
if (node.value && node.value.length > 100) {
complexConditionals.push({
name: 'Complex Conditional',
severity: 'medium',
confidence: 0.7,
location: { line: node.start.line, column: node.start.column },
description: 'Complex conditional logic detected',
suggestion: 'Consider breaking down complex conditions into smaller, named methods'
});
}
}
});
return complexConditionals;
}
private analyzeCodeDuplication(sourceCode: string, filePath: string): CodeDuplication[] {
const duplications: CodeDuplication[] = [];
const lines = sourceCode.split('\n');
const minBlockSize = 3;
// Simple duplication detection
for (let i = 0; i < lines.length - minBlockSize; i++) {
for (let blockSize = minBlockSize; blockSize <= Math.min(10, lines.length - i); blockSize++) {
const block = lines.slice(i, i + blockSize).join('\n');
// Look for the same block elsewhere
for (let j = i + blockSize; j < lines.length - blockSize + 1; j++) {
const otherBlock = lines.slice(j, j + blockSize).join('\n');
if (block === otherBlock && block.trim().length > 0) {
duplications.push({
type: 'exact',
lines: blockSize,
locations: [
{ filePath, startLine: i + 1, endLine: i + blockSize },
{ filePath, startLine: j + 1, endLine: j + blockSize }
],
similarity: 1.0
});
}
}
}
}
return duplications;
}
private buildInheritanceHierarchy(ast: ASTNode, _language: Language): InheritanceNode[] {
const hierarchy: InheritanceNode[] = [];
const classes = this.findClasses(ast);
for (const cls of classes) {
const className = cls.name || `anonymous_${cls.start.line}`;
const methods = this.findFunctions(cls).map(f => f.name || '');
const properties = this.findProperties(cls);
// Detect inheritance (simplified)
let parent: string | undefined;
if (cls.body) {
for (const child of cls.body) {
if (child.type === 'statement' && child.value) {
const extendsMatch = child.value.match(/extends\s+(\w+)/);
const implementsMatch = child.value.match(/implements\s+(\w+)/);
if (extendsMatch) {
parent = extendsMatch[1];
} else if (implementsMatch) {
parent = implementsMatch[1];
}
}
}
}
hierarchy.push({
name: className,
type: 'class',
parent,
children: [],
methods,
properties,
location: { line: cls.start.line, column: cls.start.column }
});
}
// Build child relationships
for (const node of hierarchy) {
if (node.parent) {
const parent = hierarchy.find(n => n.name === node.parent);
if (parent) {
parent.children.push(node.name);
}
}
}
return hierarchy;
}
private analyzeInterfaces(ast: ASTNode, _language: Language): InterfaceInfo[] {
const interfaces: InterfaceInfo[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class' && node.name) {
const className = node.name;
const methods = this.findFunctions(node).map(f => f.name || '');
const properties = this.findProperties(node);
// Check if this looks like an interface
const hasInterfaceInName = className.toLowerCase().includes('interface');
const hasAllAbstractMethods = this.hasAllAbstractMethods(node, _language);
if (hasInterfaceInName || hasAllAbstractMethods) {
interfaces.push({
name: className,
methods,
properties,
implementors: [], // Would need cross-file analysis
location: { line: node.start.line, column: node.start.column }
});
}
}
});
return interfaces;
}
// Helper methods for pattern detection
private hasPrivateConstructor(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'statement' &&
child.value?.includes('private') &&
child.value?.includes('constructor')
) || false;
}
private hasStaticInstance(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'statement' &&
child.value?.includes('static') &&
child.value?.includes('instance')
) || false;
}
private hasGetInstanceMethod(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'function' &&
child.name?.toLowerCase().includes('getinstance')
) || false;
}
private hasCreateMethods(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'function' &&
child.name?.toLowerCase().includes('create')
) || false;
}
private hasAttachMethods(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'function' &&
(child.name?.toLowerCase().includes('attach') ||
child.name?.toLowerCase().includes('addobserver') ||
child.name?.toLowerCase().includes('subscribe'))
) || false;
}
private hasNotifyMethods(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'function' &&
(child.name?.toLowerCase().includes('notify') ||
child.name?.toLowerCase().includes('update'))
) || false;
}
private hasUpdateMethods(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'function' &&
child.name?.toLowerCase().includes('update')
) || false;
}
private hasAlgorithmMethods(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'function' &&
(child.name?.toLowerCase().includes('execute') ||
child.name?.toLowerCase().includes('algorithm') ||
child.name?.toLowerCase().includes('compute'))
) || false;
}
private wrapsOtherClasses(node: ASTNode, _language: Language): boolean {
// Simplified implementation
return node.body?.some(child =>
child.type === 'statement' &&
child.value?.includes('wrapper') ||
child.value?.includes('decorate')
) || false;
}
private findClasses(ast: ASTNode): ASTNode[] {
const classes: ASTNode[] = [];
this.traverseAST(ast, (node) => {
if (node.type === 'class') {
classes.push(node);
}
});
return classes;
}
private findFunctions(node: ASTNode): ASTNode[] {
const functions: ASTNode[] = [];
this.traverseAST(node, (child) => {
if (child.type === 'function' || child.type === 'method') {
functions.push(child);
}
});
return functions;
}
private findProperties(node: ASTNode): string[] {
const properties: string[] = [];
this.traverseAST(node, (child) => {
if (child.type === 'statement' && child.value) {
// Look for property declarations
const propMatches = child.value.match(/\b(?:private|public|protected)\s+(\w+)/g);
if (propMatches) {
propMatches.forEach((match: string) => {
const prop = match.split(' ')[1];
if (prop) properties.push(prop);
});
}
}
});
return properties;
}
private hasAllAbstractMethods(node: ASTNode, _language: Language): boolean {
// Simplified implementation
const methods = this.findFunctions(node);
if (methods.length === 0) return false;
return methods.every(method =>
method.body?.some(child =>
child.type === 'statement' &&
child.value?.includes('abstract')
)
);
}
private traverseAST(ast: ASTNode, visitor: (node: ASTNode) => void): void {
visitor(ast);
if (ast.body) {
for (const child of ast.body) {
this.traverseAST(child, visitor);
}
}
if (ast.children) {
for (const child of ast.children) {
this.traverseAST(child, visitor);
}
}
}
}