UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

485 lines (484 loc) 19.8 kB
import { BaseLanguageHandler } from './base.js'; import { getNodeText } from '../astAnalyzer.js'; import logger from '../../../logger.js'; export class ElixirHandler extends BaseLanguageHandler { getFunctionQueryPatterns() { return [ 'function', 'anonymous_function', 'call', 'def', 'defp' ]; } getClassQueryPatterns() { return [ 'module', 'defmodule', 'defprotocol', 'defimpl' ]; } getImportQueryPatterns() { return [ 'import', 'alias', 'require', 'use' ]; } extractFunctionName(node, sourceCode, _options) { try { if (node.type === 'function' || node.type === 'def' || node.type === 'defp') { const nameNode = node.childForFieldName('name'); if (nameNode) { const name = getNodeText(nameNode, sourceCode); if (name.startsWith('test_') || this.hasTestMacro(node, sourceCode)) { return `test_${name}`; } if (this.hasCallbackAttribute(node, sourceCode)) { return `callback_${name}`; } if (this.isPhoenixControllerAction(node, sourceCode)) { return `action_${name}`; } if (this.isGenServerCallback(name)) { return `genserver_${name}`; } const arityMatch = name.match(/^(.+)\/(\d+)$/); if (arityMatch) { return `${arityMatch[1]}_${arityMatch[2]}`; } return name; } } if (node.type === 'anonymous_function') { if (node.parent?.type === 'match') { const leftNode = node.parent.childForFieldName('left'); if (leftNode) { return getNodeText(leftNode, sourceCode); } } if (node.parent?.type === 'call' && node.parent.childForFieldName('operator')?.text === '|>') { const nameNode = node.parent.childForFieldName('name'); if (nameNode) { const funcName = getNodeText(nameNode, sourceCode); if (['map', 'filter', 'reduce', 'each'].includes(funcName)) { return `${funcName}_function`; } } } return 'anonymous_function'; } if (node.type === 'call') { const nameNode = node.childForFieldName('name'); const argsNode = node.childForFieldName('arguments'); if (nameNode && argsNode) { const name = getNodeText(nameNode, sourceCode); if (['defmacro', 'defmacrop'].includes(name)) { const macroNameNode = argsNode.firstChild; if (macroNameNode) { return `macro_${getNodeText(macroNameNode, sourceCode)}`; } } } } return 'anonymous'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Elixir function name'); return 'anonymous'; } } hasTestMacro(node, _sourceCode) { try { let current = node.previousNamedSibling; while (current) { if (current.type === 'call' && current.childForFieldName('name')?.text === '@tag') { return true; } if (current.type === 'call' && current.childForFieldName('name')?.text === 'test') { return true; } current = current.previousNamedSibling; } return false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if Elixir function has test macro'); return false; } } hasCallbackAttribute(node, _sourceCode) { try { let current = node.previousNamedSibling; while (current) { if (current.type === 'call' && current.childForFieldName('name')?.text === '@callback') { return true; } current = current.previousNamedSibling; } return false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if Elixir function has callback attribute'); return false; } } isPhoenixControllerAction(node, sourceCode) { try { let current = node.parent; while (current && current.type !== 'module' && current.type !== 'defmodule') { current = current.parent; } if (current) { const nameNode = current.childForFieldName('name'); if (nameNode) { const moduleName = getNodeText(nameNode, sourceCode); return moduleName.includes('Controller'); } } return false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if Elixir function is Phoenix controller action'); return false; } } isGenServerCallback(name) { const callbacks = [ 'init', 'handle_call', 'handle_cast', 'handle_info', 'terminate', 'code_change' ]; return callbacks.includes(name); } extractClassName(node, sourceCode) { try { if (node.type === 'module' || node.type === 'defmodule' || node.type === 'defprotocol' || node.type === 'defimpl') { const nameNode = node.childForFieldName('name'); if (nameNode) { return getNodeText(nameNode, sourceCode); } } return 'AnonymousModule'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Elixir class name'); return 'AnonymousModule'; } } extractImplementedInterfaces(node, sourceCode) { try { if (node.type === 'module' || node.type === 'defmodule') { const behaviours = []; let child = node.firstChild; while (child) { if (child.type === 'call' && child.childForFieldName('name')?.text === '@behaviour') { const argsNode = child.childForFieldName('arguments'); if (argsNode?.firstChild) { behaviours.push(getNodeText(argsNode.firstChild, sourceCode)); } } child = child.nextNamedSibling; } child = node.firstChild; while (child) { if (child.type === 'call' && child.childForFieldName('name')?.text === 'use') { const argsNode = child.childForFieldName('arguments'); if (argsNode?.firstChild) { behaviours.push(getNodeText(argsNode.firstChild, sourceCode)); } } child = child.nextNamedSibling; } return behaviours.length > 0 ? behaviours : undefined; } return undefined; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Elixir implemented interfaces'); return undefined; } } extractImportPath(node, sourceCode) { try { if (node.type === 'import' || node.type === 'alias' || node.type === 'require' || node.type === 'use') { const argsNode = node.childForFieldName('arguments'); if (argsNode?.firstChild) { return getNodeText(argsNode.firstChild, sourceCode); } } return 'unknown'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Elixir import path'); return 'unknown'; } } extractImportedItems(node, sourceCode) { try { if (node.type === 'import') { const argsNode = node.childForFieldName('arguments'); if (argsNode?.firstChild) { const moduleName = getNodeText(argsNode.firstChild, sourceCode); const options = this.extractImportOptions(argsNode, sourceCode); if (options && options.only) { return options.only.map((item) => ({ name: item.replace(/^:/, ''), path: moduleName, isDefault: false, isNamespace: false, nodeText: node.text, importType: 'import', onlyImport: true })); } if (options && options.except) { return [{ name: moduleName, path: moduleName, isDefault: false, isNamespace: true, nodeText: node.text, importType: 'import', exceptItems: options.except.map((item) => item.replace(/^:/, '')) }]; } return [{ name: moduleName, path: moduleName, isDefault: false, isNamespace: true, nodeText: node.text, importType: 'import' }]; } } else if (node.type === 'alias') { const argsNode = node.childForFieldName('arguments'); if (argsNode?.firstChild) { const modulePath = getNodeText(argsNode.firstChild, sourceCode); const options = this.extractImportOptions(argsNode, sourceCode); if (options && options.as) { return [{ name: options.as, path: modulePath, alias: options.as, isDefault: false, isNamespace: true, nodeText: node.text, importType: 'alias' }]; } if (modulePath.includes('{') && modulePath.includes('}')) { const basePath = modulePath.substring(0, modulePath.indexOf('{')); const subModulesText = modulePath.substring(modulePath.indexOf('{') + 1, modulePath.lastIndexOf('}')); const subModules = subModulesText.split(',').map(s => s.trim()); return subModules.map(subModule => ({ name: subModule, path: basePath + subModule, isDefault: false, isNamespace: true, nodeText: subModule, importType: 'alias', isMultiAlias: true })); } const parts = modulePath.split('.'); const name = parts[parts.length - 1]; return [{ name: name, path: modulePath, isDefault: false, isNamespace: true, nodeText: node.text, importType: 'alias' }]; } } else if (node.type === 'require') { const argsNode = node.childForFieldName('arguments'); if (argsNode?.firstChild) { const moduleName = getNodeText(argsNode.firstChild, sourceCode); const options = this.extractImportOptions(argsNode, sourceCode); if (options && options.as) { return [{ name: options.as, path: moduleName, alias: options.as, isDefault: false, isNamespace: true, nodeText: node.text, importType: 'require' }]; } return [{ name: moduleName, path: moduleName, isDefault: false, isNamespace: true, nodeText: node.text, importType: 'require' }]; } } else if (node.type === 'use') { const argsNode = node.childForFieldName('arguments'); if (argsNode?.firstChild) { const moduleName = getNodeText(argsNode.firstChild, sourceCode); const options = this.extractImportOptions(argsNode, sourceCode); return [{ name: moduleName, path: moduleName, isDefault: false, isNamespace: true, nodeText: node.text, importType: 'use', options: options }]; } } return undefined; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Elixir imported items'); return undefined; } } extractImportOptions(argsNode, sourceCode) { try { const optionsNode = argsNode.firstChild?.nextNamedSibling; if (!optionsNode) { return undefined; } const options = {}; const optionsText = getNodeText(optionsNode, sourceCode); const onlyMatch = optionsText.match(/only:\s*\[(.*?)\]/); if (onlyMatch && onlyMatch[1]) { options.only = onlyMatch[1].split(',').map(s => s.trim()); } const exceptMatch = optionsText.match(/except:\s*\[(.*?)\]/); if (exceptMatch && exceptMatch[1]) { options.except = exceptMatch[1].split(',').map(s => s.trim()); } const asMatch = optionsText.match(/as:\s*([A-Za-z0-9._]+)/); if (asMatch && asMatch[1]) { options.as = asMatch[1]; } return Object.keys(options).length > 0 ? options : undefined; } catch (error) { logger.warn({ err: error }, 'Error extracting Elixir import options'); return undefined; } } extractFunctionComment(node, sourceCode) { try { const current = node; let prev = current.previousNamedSibling; while (prev && prev.type !== 'comment') { prev = prev.previousNamedSibling; } if (prev && prev.type === 'comment') { const commentText = getNodeText(prev, sourceCode); return commentText .replace(/^#\s*/mg, '') .trim(); } prev = current.previousNamedSibling; while (prev) { if (prev.type === 'call' && prev.childForFieldName('name')?.text === '@doc') { const argsNode = prev.childForFieldName('arguments'); if (argsNode?.firstChild) { return getNodeText(argsNode.firstChild, sourceCode) .replace(/^["']|["']$/g, ''); } } prev = prev.previousNamedSibling; } return undefined; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Elixir function comment'); return undefined; } } extractClassComment(node, sourceCode) { try { const current = node; let prev = current.previousNamedSibling; while (prev && prev.type !== 'comment') { prev = prev.previousNamedSibling; } if (prev && prev.type === 'comment') { const commentText = getNodeText(prev, sourceCode); return commentText .replace(/^#\s*/mg, '') .trim(); } let child = node.firstChild; while (child) { if (child.type === 'call' && child.childForFieldName('name')?.text === '@moduledoc') { const argsNode = child.childForFieldName('arguments'); if (argsNode?.firstChild) { return getNodeText(argsNode.firstChild, sourceCode) .replace(/^["']|["']$/g, ''); } } child = child.nextNamedSibling; } return undefined; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Elixir class comment'); return undefined; } } detectFramework(sourceCode) { try { if (sourceCode.includes('use Phoenix.') || sourceCode.includes('Phoenix.Controller') || sourceCode.includes('Phoenix.Router')) { return 'phoenix'; } if (sourceCode.includes('use Ecto.') || sourceCode.includes('Ecto.Schema') || sourceCode.includes('Ecto.Changeset')) { return 'ecto'; } if (sourceCode.includes('use Nerves.') || sourceCode.includes('Nerves.Runtime') || sourceCode.includes('Nerves.Network')) { return 'nerves'; } if (sourceCode.includes('use Phoenix.LiveView') || sourceCode.includes('Phoenix.LiveComponent') || sourceCode.includes('mount/3')) { return 'liveview'; } return null; } catch (error) { logger.warn({ err: error }, 'Error detecting Elixir framework'); return null; } } }