UNPKG

@paulohenriquevn/m2js

Version:

Transform TypeScript/JavaScript code into LLM-friendly Markdown summaries + Smart Dead Code Detection + Graph-Deep Diff Analysis. Extract exported functions, classes, and JSDoc comments for better AI context with 60%+ token reduction. Intelligent dead cod

496 lines 16.2 kB
"use strict"; /** * Semantic Relationship Analyzer for M2JS * Analyzes business entities, relationships, and workflows */ Object.defineProperty(exports, "__esModule", { value: true }); exports.analyzeSemanticRelationships = analyzeSemanticRelationships; /** * Analyzes semantic relationships and business domain */ function analyzeSemanticRelationships(parsedFiles, dependencyGraph) { const entities = extractBusinessEntities(parsedFiles); const relationships = extractEntityRelationships(parsedFiles, dependencyGraph, entities); const workflows = extractBusinessWorkflows(parsedFiles, entities); const stateTransitions = extractStateTransitions(parsedFiles, entities); const invariants = extractBusinessInvariants(parsedFiles, entities); return { entities, relationships, workflows, stateTransitions, invariants, }; } /** * Extracts business entities from classes and their usage */ function extractBusinessEntities(parsedFiles) { const entities = []; parsedFiles.forEach(file => { file.classes.forEach(cls => { const entity = analyzeClassAsEntity(cls, file); if (entity) { entities.push(entity); } }); }); return entities; } /** * Analyzes a class to determine if it's a business entity */ function analyzeClassAsEntity(cls, file) { // Skip utility classes and technical classes if (isUtilityClass(cls.name)) { return null; } const properties = extractEntityProperties(cls); const operations = extractEntityOperations(cls); const entityType = determineEntityType(cls, operations); return { name: cls.name, type: entityType, properties, operations, description: extractEntityDescription(cls, file), }; } /** * Determines if a class is a utility class */ function isUtilityClass(className) { const utilityPatterns = [ 'util', 'helper', 'validator', 'formatter', 'converter', 'parser', 'builder', 'factory', 'manager', 'handler', ]; const lowerName = className.toLowerCase(); return utilityPatterns.some(pattern => lowerName.includes(pattern)); } /** * Extracts entity properties from class methods */ function extractEntityProperties(cls) { const properties = []; // Look for getter/setter patterns to infer properties const getters = cls.methods.filter((m) => m.name.startsWith('get') && m.params.length === 0); const setters = cls.methods.filter((m) => m.name.startsWith('set') && m.params.length === 1); getters.forEach((getter) => { const propName = getter.name.substring(3).toLowerCase(); const setter = setters.find((s) => s.name.toLowerCase() === `set${propName}`); properties.push({ name: propName, type: getter.returnType || 'unknown', required: !setter, // If no setter, might be required/readonly description: getter.jsDoc ? extractFirstLine(getter.jsDoc) : undefined, }); }); // If no clear getter/setter pattern, infer from constructor or common patterns if (properties.length === 0) { properties.push(...inferPropertiesFromContext(cls)); } return properties; } /** * Infers properties from class context */ function inferPropertiesFromContext(cls) { const properties = []; // Common business entity properties const commonProps = [ 'id', 'name', 'email', 'status', 'createdAt', 'updatedAt', ]; commonProps.forEach(prop => { const hasRelatedMethod = cls.methods.some((m) => m.name.toLowerCase().includes(prop.toLowerCase())); if (hasRelatedMethod) { properties.push({ name: prop, type: prop === 'id' ? 'string' : prop.includes('At') ? 'Date' : 'string', required: prop === 'id' || prop === 'name', }); } }); return properties; } /** * Extracts entity operations from class methods */ function extractEntityOperations(cls) { const operations = []; cls.methods.forEach((method) => { const operation = classifyOperation(method); if (operation) { operations.push(operation); } }); return operations; } /** * Classifies a method as a type of operation */ function classifyOperation(method) { const name = method.name.toLowerCase(); // const hasReturnValue = method.returnType && method.returnType !== 'void'; const hasSideEffects = name.includes('create') || name.includes('update') || name.includes('delete') || name.includes('save'); let type = 'query'; if (hasSideEffects) { type = 'command'; } else if (name.includes('notify') || name.includes('emit') || name.includes('publish')) { type = 'event'; } return { name: method.name, type, parameters: method.params.map((p) => `${p.name}: ${p.type || 'unknown'}`), returns: method.returnType || 'void', sideEffects: hasSideEffects, }; } /** * Determines the type of entity based on patterns */ function determineEntityType(cls, operations) { const className = cls.name.toLowerCase(); const hasCommands = operations.some(op => op.type === 'command'); const hasQueries = operations.some(op => op.type === 'query'); if (className.includes('service') || className.includes('manager')) { return 'service'; } if (hasCommands && hasQueries) { return 'aggregate'; // Rich domain object } if (hasQueries && !hasCommands) { return 'value-object'; // Immutable object } return 'entity'; // Basic entity } /** * Extracts entity description from JSDoc or class name */ function extractEntityDescription(cls, file) { if (cls.jsDoc) { const firstLine = extractFirstLine(cls.jsDoc); if (firstLine && firstLine.length > 10) { return firstLine; } } // Generate description from class name and file context const className = cls.name.replace(/([A-Z])/g, ' $1').trim(); const fileName = file.fileName.replace(/([A-Z])/g, ' $1').trim(); return `${className} entity from ${fileName} module`; } /** * Extracts entity relationships from dependencies and usage patterns */ function extractEntityRelationships(parsedFiles, dependencyGraph, entities) { const relationships = []; // Analyze dependencies between entity files entities.forEach(entity => { const entityFile = findEntityFile(entity, parsedFiles); if (!entityFile) return; const dependencies = dependencyGraph.edges.filter(edge => edge.from === entityFile.filePath && !edge.isExternal); dependencies.forEach(dep => { const targetEntity = findEntityByFile(dep.to, entities, parsedFiles); if (targetEntity && targetEntity !== entity) { const relationship = inferRelationshipType(entity, targetEntity, entityFile); if (relationship) { relationships.push(relationship); } } }); }); return relationships; } /** * Finds the file containing an entity */ function findEntityFile(entity, parsedFiles) { return (parsedFiles.find(file => file.classes.some(cls => cls.name === entity.name)) || null); } /** * Finds entity by file path */ function findEntityByFile(filePath, entities, parsedFiles) { const file = parsedFiles.find(f => f.filePath === filePath); if (!file) return null; for (const cls of file.classes) { const entity = entities.find(e => e.name === cls.name); if (entity) return entity; } return null; } /** * Infers relationship type between entities */ function inferRelationshipType(from, to, _fromFile) { const fromName = from.name.toLowerCase(); const toName = to.name.toLowerCase(); // Common relationship patterns if (fromName.includes('user') && toName.includes('order')) { return { from: from.name, to: to.name, type: 'has-many', cardinality: '1:N', description: `${from.name} can have multiple ${to.name}s`, }; } if (fromName.includes('order') && toName.includes('payment')) { return { from: from.name, to: to.name, type: 'has-one', cardinality: '1:1', description: `${from.name} has one ${to.name}`, }; } // Service relationships if (from.type === 'service' && to.type !== 'service') { return { from: from.name, to: to.name, type: 'uses', cardinality: '1:N', description: `${from.name} service operates on ${to.name} entities`, }; } // Default dependency relationship return { from: from.name, to: to.name, type: 'depends-on', cardinality: '1:1', description: `${from.name} depends on ${to.name}`, }; } /** * Extracts business workflows from function sequences */ function extractBusinessWorkflows(parsedFiles, entities) { const workflows = []; // Look for service classes that orchestrate workflows const serviceFiles = parsedFiles.filter(file => file.fileName.toLowerCase().includes('service') || file.classes.some(cls => cls.name.toLowerCase().includes('service'))); serviceFiles.forEach(file => { file.functions.forEach(func => { const workflow = analyzeWorkflowFunction(func, entities); if (workflow) { workflows.push(workflow); } }); file.classes.forEach(cls => { cls.methods.forEach(method => { const workflow = analyzeWorkflowMethod(method, cls.name, entities); if (workflow) { workflows.push(workflow); } }); }); }); return workflows.slice(0, 5); // Limit to most relevant workflows } /** * Analyzes a function as a potential workflow */ function analyzeWorkflowFunction(func, entities) { const name = func.name.toLowerCase(); // Look for workflow indicators if (!name.includes('process') && !name.includes('create') && !name.includes('handle') && !name.includes('execute')) { return null; } const workflowName = func.name.replace(/([A-Z])/g, ' $1').trim(); const description = func.jsDoc ? extractFirstLine(func.jsDoc) : `${workflowName} workflow`; const steps = inferWorkflowSteps(func, entities); return { name: workflowName, description, steps, triggers: [`${func.name}() function call`], outcomes: [func.returnType || 'completion'], }; } /** * Analyzes a method as a potential workflow */ function analyzeWorkflowMethod(method, className, entities) { const name = method.name.toLowerCase(); if (!name.includes('process') && !name.includes('create') && !name.includes('handle') && !name.includes('execute')) { return null; } const workflowName = `${className}.${method.name}`; const description = method.jsDoc ? extractFirstLine(method.jsDoc) : `${workflowName} workflow`; const steps = inferWorkflowSteps(method, entities); return { name: workflowName, description, steps, triggers: [`${method.name}() method call`], outcomes: [method.returnType || 'completion'], }; } /** * Infers workflow steps from function/method analysis */ function inferWorkflowSteps(func, _entities) { const steps = []; // This is a simplified inference // In practice, you'd analyze the function body AST if (func.params.length > 0) { steps.push({ order: 1, action: 'Validate Input', actor: 'System', preconditions: ['Input parameters provided'], postconditions: ['Input validated'], }); } if (func.name.includes('create')) { steps.push({ order: 2, action: 'Create Entity', actor: 'Business Logic', preconditions: ['Valid input'], postconditions: ['Entity created'], }); } if (func.returnType?.includes('Promise') || func.name.includes('save')) { steps.push({ order: steps.length + 1, action: 'Persist Changes', actor: 'Data Layer', preconditions: ['Entity ready'], postconditions: ['Changes persisted'], }); } return steps; } /** * Extracts state transitions from entity analysis */ function extractStateTransitions(_parsedFiles, entities) { const transitions = []; entities.forEach(entity => { const stateTransition = analyzeEntityStates(entity); if (stateTransition && stateTransition.states.length > 1) { transitions.push(stateTransition); } }); return transitions; } /** * Analyzes an entity for state patterns */ function analyzeEntityStates(entity) { const entityName = entity.name.toLowerCase(); // Common state patterns const statePatterns = { order: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'], user: ['inactive', 'active', 'suspended', 'deleted'], payment: ['pending', 'processing', 'completed', 'failed', 'refunded'], product: ['draft', 'active', 'inactive', 'discontinued'], }; let states = []; // Find matching pattern for (const [pattern, patternStates] of Object.entries(statePatterns)) { if (entityName.includes(pattern)) { states = patternStates; break; } } if (states.length === 0) { return null; } // Generate basic transitions (sequential) const transitions = []; for (let i = 0; i < states.length - 1; i++) { transitions.push({ from: states[i], to: states[i + 1], trigger: `update${entity.name}Status`, conditions: [`Valid transition from ${states[i]}`], }); } return { entity: entity.name, states, transitions, invariants: [`${entity.name} can only transition to valid next states`], }; } /** * Extracts business invariants from entity operations */ function extractBusinessInvariants(_parsedFiles, entities) { const invariants = []; entities.forEach(entity => { const entityInvariants = analyzeEntityInvariants(entity); invariants.push(...entityInvariants); }); return invariants; } /** * Analyzes an entity for business invariants */ function analyzeEntityInvariants(entity) { const invariants = []; // Common invariants based on entity type and operations const validationOps = entity.operations.filter(op => op.name.toLowerCase().includes('validate')); if (validationOps.length > 0) { invariants.push({ entity: entity.name, rule: `${entity.name} must pass validation before persistence`, enforcement: 'Validation methods called before state changes', violations: ['Attempting to save invalid entity'], }); } const requiredProps = entity.properties.filter(prop => prop.required); if (requiredProps.length > 0) { invariants.push({ entity: entity.name, rule: `Required properties must be set: ${requiredProps.map(p => p.name).join(', ')}`, enforcement: 'Constructor validation and setter guards', violations: ['Missing required properties'], }); } return invariants; } /** * Extracts first line from JSDoc or comment */ function extractFirstLine(jsDoc) { const match = jsDoc.match(/\*\s*(.+?)(?:\n|\*\/)/); return match && match[1] ? match[1].trim() : ''; } //# sourceMappingURL=semantic-analyzer.js.map