/**
* Difference Analyzer for API Interface Comparison
*
* Compares frontend and backend code to detect API interface mismatches,
* including endpoints, parameters, response types, and other inconsistencies.
*/
import { Logger } from '../../utils/logger/index.js';
import { CodeParser, type ParsedCode, type ApiEndpoint } from '../code-parser/index.js';
// import { SchemaValidator } from '../schema/index.js';
import type { ValidationConfig } from '../../utils/config/index.js';
export interface DiffAnalysisRequest {
frontendCode: string;
backendCode: string;
frontendLanguage: string;
backendLanguage: string;
specPath?: string;
options?: DiffAnalysisOptions;
}
export interface DiffAnalysisOptions {
includeWarnings?: boolean;
strictMode?: boolean;
ignoreMinorDifferences?: boolean;
customRules?: DiffRule[];
}
export interface DiffRule {
name: string;
description: string;
severity: 'error' | 'warning' | 'info';
check: (frontend: ParsedCode, backend: ParsedCode) => DiffIssue[];
}
export interface DiffAnalysisResult {
isCompatible: boolean;
issues: DiffIssue[];
summary: DiffSummary;
recommendations: DiffRecommendation[];
metadata: DiffMetadata;
}
export interface DiffIssue {
type: DiffIssueType;
severity: 'error' | 'warning' | 'info';
message: string;
description: string;
frontendLocation?: SourceLocation;
backendLocation?: SourceLocation;
expectedValue?: any;
actualValue?: any;
rule?: string;
fix?: DiffFix;
}
export type DiffIssueType =
| 'endpoint_missing'
| 'endpoint_extra'
| 'method_mismatch'
| 'parameter_missing'
| 'parameter_extra'
| 'parameter_type_mismatch'
| 'response_type_mismatch'
| 'schema_mismatch'
| 'authentication_mismatch'
| 'header_mismatch'
| 'status_code_mismatch';
export interface SourceLocation {
line: number;
column: number;
file?: string;
}
export interface DiffFix {
description: string;
frontendChanges?: CodeChange[];
backendChanges?: CodeChange[];
automatic?: boolean;
}
export interface CodeChange {
type: 'add' | 'remove' | 'modify';
location: SourceLocation;
oldCode?: string;
newCode: string;
description: string;
}
export interface DiffSummary {
totalIssues: number;
errorCount: number;
warningCount: number;
infoCount: number;
compatibilityScore: number; // 0-100
affectedEndpoints: string[];
missingEndpoints: string[];
extraEndpoints: string[];
}
export interface DiffRecommendation {
priority: 'high' | 'medium' | 'low';
category: 'security' | 'performance' | 'compatibility' | 'maintainability';
message: string;
action: string;
impact: string;
}
export interface DiffMetadata {
timestamp: string;
duration: number;
frontendAnalysis: {
endpointCount: number;
classCount: number;
methodCount: number;
};
backendAnalysis: {
endpointCount: number;
classCount: number;
methodCount: number;
};
rulesApplied: string[];
}
export class DiffAnalyzer {
private logger: Logger;
private codeParser: CodeParser;
// private _schemaValidator?: SchemaValidator;
private defaultRules: DiffRule[];
constructor(_config: ValidationConfig, logger: Logger) {
this.logger = logger;
this.codeParser = new CodeParser();
this.defaultRules = this.createDefaultRules();
}
/**
* Initialize the difference analyzer
*/
async initialize(): Promise<void> {
try {
this.logger.info('Initializing Difference Analyzer...');
// CodeParser doesn't need initialization
this.logger.info('Difference Analyzer initialized successfully');
} catch (error) {
this.logger.error('Failed to initialize Difference Analyzer:', error);
throw error;
}
}
/**
* Analyze differences between frontend and backend code
*/
async analyzeDifferences(request: DiffAnalysisRequest): Promise<DiffAnalysisResult> {
const startTime = Date.now();
try {
this.logger.info('Starting difference analysis', {
frontendLanguage: request.frontendLanguage,
backendLanguage: request.backendLanguage
});
// Parse frontend and backend code with error handling
let frontendParsed: ParsedCode;
let backendParsed: ParsedCode;
try {
frontendParsed = await this.codeParser.parseCode(
request.frontendCode,
request.frontendLanguage as 'typescript' | 'javascript' | 'python' | 'java',
{ extractApiEndpoints: true, includeLocations: true }
);
} catch (error) {
this.logger.warn('Failed to parse frontend code, using empty result:', error);
frontendParsed = this.createEmptyParsedCode();
}
try {
backendParsed = await this.codeParser.parseCode(
request.backendCode,
request.backendLanguage as 'typescript' | 'javascript' | 'python' | 'java',
{ extractApiEndpoints: true, includeLocations: true }
);
} catch (error) {
this.logger.warn('Failed to parse backend code, using empty result:', error);
backendParsed = this.createEmptyParsedCode();
}
// Apply analysis rules
const issues = await this.applyAnalysisRules(
frontendParsed,
backendParsed,
request.options || {}
);
// Generate summary
const summary = this.generateSummary(issues, frontendParsed, backendParsed);
// Generate recommendations
const recommendations = this.generateRecommendations(issues, summary);
// Create metadata
const metadata: DiffMetadata = {
timestamp: new Date().toISOString(),
duration: Date.now() - startTime,
frontendAnalysis: {
endpointCount: frontendParsed.apiEndpoints.length,
classCount: frontendParsed.classes.length,
methodCount: frontendParsed.functions.length
},
backendAnalysis: {
endpointCount: backendParsed.apiEndpoints.length,
classCount: backendParsed.classes.length,
methodCount: backendParsed.functions.length
},
rulesApplied: this.defaultRules.map(rule => rule.name)
};
const result: DiffAnalysisResult = {
isCompatible: summary.errorCount === 0,
issues,
summary,
recommendations,
metadata
};
this.logger.info('Difference analysis completed', {
duration: metadata.duration,
issueCount: issues.length,
isCompatible: result.isCompatible
});
return result;
} catch (error) {
this.logger.error('Difference analysis failed:', error);
throw new Error(`Difference analysis failed: ${error}`);
}
}
/**
* Apply analysis rules to detect differences
*/
private async applyAnalysisRules(
frontend: ParsedCode,
backend: ParsedCode,
options: DiffAnalysisOptions
): Promise<DiffIssue[]> {
const issues: DiffIssue[] = [];
const rules = [...this.defaultRules, ...(options.customRules || [])];
for (const rule of rules) {
try {
const ruleIssues = rule.check(frontend, backend);
issues.push(...ruleIssues);
} catch (error) {
this.logger.warn(`Rule ${rule.name} failed:`, error);
}
}
// Filter issues based on options
let filteredIssues = issues;
if (!options.includeWarnings) {
filteredIssues = filteredIssues.filter(issue => issue.severity !== 'warning');
}
if (options.ignoreMinorDifferences) {
filteredIssues = filteredIssues.filter(issue =>
issue.severity === 'error' ||
(issue.severity === 'warning' && issue.type !== 'parameter_extra')
);
}
return filteredIssues;
}
/**
* Generate analysis summary
*/
private generateSummary(
issues: DiffIssue[],
frontend: ParsedCode,
backend: ParsedCode
): DiffSummary {
const errorCount = issues.filter(i => i.severity === 'error').length;
const warningCount = issues.filter(i => i.severity === 'warning').length;
const infoCount = issues.filter(i => i.severity === 'info').length;
// Calculate compatibility score (0-100)
const totalPossibleIssues = Math.max(
frontend.apiEndpoints.length + backend.apiEndpoints.length,
1
);
const compatibilityScore = Math.max(
0,
Math.round(100 - (errorCount * 10 + warningCount * 5) / totalPossibleIssues * 100)
);
// Extract affected endpoints
const affectedEndpoints = Array.from(new Set(
issues
.filter(issue => issue.frontendLocation || issue.backendLocation)
.map(issue => this.extractEndpointFromIssue(issue, frontend, backend))
.filter(Boolean)
)) as string[];
// Find missing and extra endpoints
const frontendPaths = new Set(frontend.apiEndpoints.map(ep => `${ep.method} ${ep.path}`));
const backendPaths = new Set(backend.apiEndpoints.map(ep => `${ep.method} ${ep.path}`));
const missingEndpoints = Array.from(backendPaths).filter(path => !frontendPaths.has(path));
const extraEndpoints = Array.from(frontendPaths).filter(path => !backendPaths.has(path));
return {
totalIssues: issues.length,
errorCount,
warningCount,
infoCount,
compatibilityScore,
affectedEndpoints,
missingEndpoints,
extraEndpoints
};
}
/**
* Generate recommendations based on analysis results
*/
private generateRecommendations(
issues: DiffIssue[],
summary: DiffSummary
): DiffRecommendation[] {
const recommendations: DiffRecommendation[] = [];
// High priority recommendations for errors
if (summary.errorCount > 0) {
recommendations.push({
priority: 'high',
category: 'compatibility',
message: `Found ${summary.errorCount} critical compatibility issues`,
action: 'Fix endpoint mismatches and parameter type errors immediately',
impact: 'Application may fail at runtime due to API incompatibilities'
});
}
// Missing endpoints recommendation
if (summary.missingEndpoints.length > 0) {
recommendations.push({
priority: 'high',
category: 'compatibility',
message: `${summary.missingEndpoints.length} backend endpoints not implemented in frontend`,
action: 'Implement missing API calls in frontend code',
impact: 'Features may not work as expected due to missing API integration'
});
}
// Extra endpoints recommendation
if (summary.extraEndpoints.length > 0) {
recommendations.push({
priority: 'medium',
category: 'maintainability',
message: `${summary.extraEndpoints.length} frontend API calls not found in backend`,
action: 'Remove unused API calls or implement corresponding backend endpoints',
impact: 'Dead code and potential runtime errors'
});
}
// Low compatibility score
if (summary.compatibilityScore < 70) {
recommendations.push({
priority: 'high',
category: 'compatibility',
message: `Low compatibility score: ${summary.compatibilityScore}%`,
action: 'Review and fix API interface mismatches systematically',
impact: 'High risk of integration failures and runtime errors'
});
}
// Security recommendations
const authIssues = issues.filter(i => i.type === 'authentication_mismatch');
if (authIssues.length > 0) {
recommendations.push({
priority: 'high',
category: 'security',
message: 'Authentication mismatches detected',
action: 'Ensure consistent authentication implementation across frontend and backend',
impact: 'Security vulnerabilities and access control issues'
});
}
return recommendations;
}
/**
* Extract endpoint information from issue
*/
private extractEndpointFromIssue(
issue: DiffIssue,
frontend: ParsedCode,
backend: ParsedCode
): string | null {
// Try to find endpoint based on location
if (issue.frontendLocation) {
const endpoint = frontend.apiEndpoints.find(ep =>
ep.location.start.line === issue.frontendLocation!.line
);
if (endpoint) {
return `${endpoint.method} ${endpoint.path}`;
}
}
if (issue.backendLocation) {
const endpoint = backend.apiEndpoints.find(ep =>
ep.location.start.line === issue.backendLocation!.line
);
if (endpoint) {
return `${endpoint.method} ${endpoint.path}`;
}
}
return null;
}
/**
* Create default analysis rules
*/
private createDefaultRules(): DiffRule[] {
return [
{
name: 'endpoint_consistency',
description: 'Check for missing or extra endpoints between frontend and backend',
severity: 'error',
check: (frontend, backend) => this.checkEndpointConsistency(frontend, backend)
},
{
name: 'method_consistency',
description: 'Verify HTTP methods match between frontend and backend',
severity: 'error',
check: (frontend, backend) => this.checkMethodConsistency(frontend, backend)
},
{
name: 'parameter_consistency',
description: 'Check parameter types and requirements',
severity: 'warning',
check: (frontend, backend) => this.checkParameterConsistency(frontend, backend)
},
{
name: 'response_type_consistency',
description: 'Verify response types match expectations',
severity: 'warning',
check: (frontend, backend) => this.checkResponseTypeConsistency(frontend, backend)
}
];
}
/**
* Check endpoint consistency between frontend and backend
*/
private checkEndpointConsistency(frontend: ParsedCode, backend: ParsedCode): DiffIssue[] {
const issues: DiffIssue[] = [];
const frontendEndpoints = new Map(
frontend.apiEndpoints.map(ep => [`${ep.method}:${ep.path}`, ep])
);
const backendEndpoints = new Map(
backend.apiEndpoints.map(ep => [`${ep.method}:${ep.path}`, ep])
);
// Check for missing endpoints in frontend
for (const [key, backendEp] of backendEndpoints) {
if (!frontendEndpoints.has(key)) {
issues.push({
type: 'endpoint_missing',
severity: 'error',
message: `Missing frontend implementation for ${backendEp.method} ${backendEp.path}`,
description: 'Backend endpoint exists but no corresponding frontend API call found',
backendLocation: {
line: backendEp.location.start.line,
column: backendEp.location.start.column
},
expectedValue: `${backendEp.method} ${backendEp.path}`,
rule: 'endpoint_consistency'
});
}
}
// Check for extra endpoints in frontend
for (const [key, frontendEp] of frontendEndpoints) {
if (!backendEndpoints.has(key)) {
issues.push({
type: 'endpoint_extra',
severity: 'warning',
message: `Extra frontend API call ${frontendEp.method} ${frontendEp.path}`,
description: 'Frontend API call exists but no corresponding backend endpoint found',
frontendLocation: {
line: frontendEp.location.start.line,
column: frontendEp.location.start.column
},
actualValue: `${frontendEp.method} ${frontendEp.path}`,
rule: 'endpoint_consistency'
});
}
}
return issues;
}
/**
* Check HTTP method consistency
*/
private checkMethodConsistency(frontend: ParsedCode, backend: ParsedCode): DiffIssue[] {
const issues: DiffIssue[] = [];
// Create maps for easier comparison
const frontendByPath = new Map<string, ApiEndpoint[]>();
const backendByPath = new Map<string, ApiEndpoint[]>();
// Group endpoints by path
frontend.apiEndpoints.forEach(ep => {
const endpoints = frontendByPath.get(ep.path) || [];
endpoints.push(ep);
frontendByPath.set(ep.path, endpoints);
});
backend.apiEndpoints.forEach(ep => {
const endpoints = backendByPath.get(ep.path) || [];
endpoints.push(ep);
backendByPath.set(ep.path, endpoints);
});
// Check for method mismatches on same paths
for (const [path, frontendEps] of frontendByPath) {
const backendEps = backendByPath.get(path);
if (!backendEps) continue;
// const _frontendMethods = new Set(frontendEps.map(ep => ep.method));
const backendMethods = new Set(backendEps.map(ep => ep.method));
// Check for method mismatches
for (const frontendEp of frontendEps) {
if (!backendMethods.has(frontendEp.method)) {
const suggestedMethod = this.findSimilarMethod(frontendEp.method, Array.from(backendMethods));
issues.push({
type: 'method_mismatch',
severity: 'error',
message: `Method mismatch for ${path}: frontend uses ${frontendEp.method}, backend doesn't support it`,
description: suggestedMethod
? `Consider using ${suggestedMethod} instead of ${frontendEp.method}`
: `Backend doesn't support ${frontendEp.method} method for ${path}`,
frontendLocation: {
line: frontendEp.location.start.line,
column: frontendEp.location.start.column
},
expectedValue: Array.from(backendMethods),
actualValue: frontendEp.method,
rule: 'method_consistency'
});
}
}
}
return issues;
}
/**
* Check parameter consistency
*/
private checkParameterConsistency(frontend: ParsedCode, backend: ParsedCode): DiffIssue[] {
const issues: DiffIssue[] = [];
// Compare parameters for matching endpoints
const frontendEndpoints = new Map(
frontend.apiEndpoints.map(ep => [`${ep.method}:${ep.path}`, ep])
);
const backendEndpoints = new Map(
backend.apiEndpoints.map(ep => [`${ep.method}:${ep.path}`, ep])
);
for (const [key, frontendEp] of frontendEndpoints) {
const backendEp = backendEndpoints.get(key);
if (!backendEp) continue;
// Compare parameters
const frontendParams = new Map(frontendEp.parameters.map(p => [p.name, p]));
const backendParams = new Map(backendEp.parameters.map(p => [p.name, p]));
// Check for missing parameters in frontend
for (const [paramName, backendParam] of backendParams) {
const frontendParam = frontendParams.get(paramName);
if (!frontendParam) {
issues.push({
type: 'parameter_missing',
severity: 'warning',
message: `Missing parameter '${paramName}' in frontend call to ${frontendEp.method} ${frontendEp.path}`,
description: `Backend expects parameter '${paramName}' of type ${backendParam.type}`,
frontendLocation: {
line: frontendEp.location.start.line,
column: frontendEp.location.start.column
},
expectedValue: backendParam,
rule: 'parameter_consistency'
});
} else if (frontendParam.type !== backendParam.type &&
frontendParam.type !== 'any' && backendParam.type !== 'any') {
// Type mismatch
issues.push({
type: 'parameter_type_mismatch',
severity: 'warning',
message: `Parameter type mismatch for '${paramName}' in ${frontendEp.method} ${frontendEp.path}`,
description: `Frontend expects ${frontendParam.type}, backend expects ${backendParam.type}`,
frontendLocation: {
line: frontendEp.location.start.line,
column: frontendEp.location.start.column
},
expectedValue: backendParam.type,
actualValue: frontendParam.type,
rule: 'parameter_consistency'
});
}
}
// Check for extra parameters in frontend
for (const [paramName, frontendParam] of frontendParams) {
if (!backendParams.has(paramName)) {
issues.push({
type: 'parameter_extra',
severity: 'info',
message: `Extra parameter '${paramName}' in frontend call to ${frontendEp.method} ${frontendEp.path}`,
description: `Frontend sends parameter '${paramName}' but backend doesn't expect it`,
frontendLocation: {
line: frontendEp.location.start.line,
column: frontendEp.location.start.column
},
actualValue: frontendParam,
rule: 'parameter_consistency'
});
}
}
}
return issues;
}
/**
* Check response type consistency
*/
private checkResponseTypeConsistency(frontend: ParsedCode, backend: ParsedCode): DiffIssue[] {
const issues: DiffIssue[] = [];
// Compare response types for matching endpoints
const frontendEndpoints = new Map(
frontend.apiEndpoints.map(ep => [`${ep.method}:${ep.path}`, ep])
);
const backendEndpoints = new Map(
backend.apiEndpoints.map(ep => [`${ep.method}:${ep.path}`, ep])
);
for (const [key, frontendEp] of frontendEndpoints) {
const backendEp = backendEndpoints.get(key);
if (!backendEp) continue;
// Compare response types
if (frontendEp.responseType && backendEp.responseType &&
frontendEp.responseType !== backendEp.responseType &&
frontendEp.responseType !== 'any' && backendEp.responseType !== 'any') {
issues.push({
type: 'response_type_mismatch',
severity: 'warning',
message: `Response type mismatch for ${frontendEp.method} ${frontendEp.path}`,
description: `Frontend expects ${frontendEp.responseType}, backend returns ${backendEp.responseType}`,
frontendLocation: {
line: frontendEp.location.start.line,
column: frontendEp.location.start.column
},
backendLocation: {
line: backendEp.location.start.line,
column: backendEp.location.start.column
},
expectedValue: backendEp.responseType,
actualValue: frontendEp.responseType,
rule: 'response_type_consistency'
});
}
}
return issues;
}
/**
* Find similar HTTP method for suggestions
*/
private findSimilarMethod(method: string, availableMethods: string[]): string | null {
const methodMap: Record<string, string[]> = {
'GET': ['POST', 'PUT'],
'POST': ['PUT', 'PATCH', 'GET'],
'PUT': ['POST', 'PATCH'],
'PATCH': ['PUT', 'POST'],
'DELETE': ['POST', 'PUT']
};
const suggestions = methodMap[method] || [];
return suggestions.find(suggestion => availableMethods.includes(suggestion)) || null;
}
/**
* Create empty parsed code result for error cases
*/
private createEmptyParsedCode(): ParsedCode {
return {
language: 'typescript',
classes: [],
interfaces: [],
functions: [],
imports: [],
exports: [],
apiEndpoints: [],
metadata: {
totalLines: 0,
codeLines: 0,
commentLines: 0,
complexity: 0,
dependencies: []
}
};
}
/**
* Get analysis statistics
*/
getStats(): { rulesCount: number } {
return {
rulesCount: this.defaultRules.length
};
}
}