UNPKG

meld

Version:

Meld: A template language for LLM prompts

234 lines (203 loc) 8.2 kB
/** * sourceMapUtils.ts * * Utility functions for working with source maps and enhancing errors with source location information. */ import { sourceMapService, SourceLocation } from './SourceMapService.js'; import { MeldError, MeldErrorOptions, ErrorSeverity } from '@core/errors/MeldError.js'; import { logger } from './logger.js'; /** * Extract line and column numbers from an error message * @param error The error to extract location from * @returns Location object or null if not found */ export function extractErrorLocation(error: Error): { line: number; column: number } | null { // First, check if the error message contains multiple line/column references // (e.g., "Directive error (embed): ... at line 29, column 2 at line 29, column 2") // This happens with nested errors, and we want the last (most specific) one let matches = []; const errorMsg = error.message || ''; // Common patterns to extract line/column information const lineColPatterns = [ /(?:at|on|in|line)\s+(?:line\s+)?(\d+)(?:,\s+column\s+|:)(\d+)/gi, // "at line 10:20" or "line 10, column 20" /line\s+(\d+)(?:\s+|,\s+)(?:column|col|position|char|character)\s+(\d+)/gi, // "line 10 column 20" /\[(\d+),\s*(\d+)\]/g, // "[10, 20]" /\((\d+):(\d+)\)/g // "(10:20)" ]; // Try to find all matches in the error message for (const pattern of lineColPatterns) { let match; // Reset the regex to start from the beginning pattern.lastIndex = 0; while ((match = pattern.exec(errorMsg)) !== null) { if (match && match.length >= 3) { const line = parseInt(match[1], 10); const column = parseInt(match[2], 10); // Validate numbers are reasonable if (!isNaN(line) && !isNaN(column) && line > 0 && column >= 0) { matches.push({ line, column, index: match.index }); } } } } // If we found multiple matches, prefer the last one (most specific) if (matches.length > 0) { // Sort by position in the string (later matches are more specific) matches.sort((a, b) => b.index - a.index); const lastMatch = matches[0]; logger.debug(`Extracted location from error message: ${lastMatch.line}:${lastMatch.column} (from ${matches.length} matches)`, { message: error.message, allMatches: matches.map(m => `${m.line}:${m.column}`) }); return { line: lastMatch.line, column: lastMatch.column }; } logger.debug(`Could not extract location from error message: ${error.message}`); return null; } /** * Extract location information from an error object * Tries various properties where location might be stored * @param error Error object to extract location from * @returns Location object or null if not found */ export function extractLocationFromErrorObject(error: any): { line: number; column: number } | null { // Check for standard location property patterns if (error.location) { // Parse MeldParseError style location: { start: { line, column }, end: { line, column } } if (error.location.start && typeof error.location.start.line === 'number' && typeof error.location.start.column === 'number') { logger.debug(`Extracted location from error.location.start: ${error.location.start.line}:${error.location.start.column}`); return { line: error.location.start.line, column: error.location.start.column }; } // Parse location: { line, column } if (typeof error.location.line === 'number' && typeof error.location.column === 'number') { logger.debug(`Extracted location from error.location: ${error.location.line}:${error.location.column}`); return { line: error.location.line, column: error.location.column }; } } // Check for LLMXML style position info if (error.details?.node?.position?.start) { const { line, column } = error.details.node.position.start; if (typeof line === 'number' && typeof column === 'number') { logger.debug(`Extracted location from error.details.node.position.start: ${line}:${column}`); return { line, column }; } } // Check for position property directly if (error.position?.start) { const { line, column } = error.position.start; if (typeof line === 'number' && typeof column === 'number') { logger.debug(`Extracted location from error.position.start: ${line}:${column}`); return { line, column }; } } // If no structured location found, try extracting from the message return extractErrorLocation(error); } /** * Enhance a MeldError with source location information * @param error The error to enhance * @param options Options for enhancement * @returns Enhanced error with source location information */ export function enhanceMeldErrorWithSourceInfo( error: MeldError, options?: { preferExistingSourceInfo?: boolean } ): MeldError { // Extract location from error object or message const location = extractLocationFromErrorObject(error); // Skip enhancement if location can't be extracted if (!location) { logger.debug(`Could not enhance error: ${error.message} (no location found)`); return error; } // Try to find the original source location const sourceLocation = sourceMapService.findOriginalLocation( location.line, location.column ); // Skip if source location not found if (!sourceLocation) { logger.debug(`Could not enhance error: ${error.message} (no source mapping found for ${location.line}:${location.column})`); return error; } // If error already has a filePath and we should prefer it, don't override if (options?.preferExistingSourceInfo && error.filePath) { logger.debug(`Not enhancing error: ${error.message} (keeping existing filePath ${error.filePath})`); return error; } // Create new error options with source info const newOptions: MeldErrorOptions = { code: error.code, severity: error.severity, context: error.context ? { ...error.context } : {}, filePath: sourceLocation.filePath, cause: (error as any).errorCause }; // Add sourceLocation to context newOptions.context!.sourceLocation = sourceLocation; // Create enhanced message const enhancedMessage = `${error.message} (in ${sourceLocation.filePath}:${sourceLocation.line})`; // Create a new error of the same type with enhanced information const EnhancedErrorConstructor = Object.getPrototypeOf(error).constructor; const enhancedError = new EnhancedErrorConstructor(enhancedMessage, newOptions); logger.debug(`Enhanced error: "${error.message}" -> "${enhancedError.message}"`, { originalLocation: location, sourceLocation: sourceLocation, filePath: sourceLocation.filePath }); return enhancedError; } /** * Helper to register source file content * @param filePath Path to the source file * @param content Content of the source file */ export function registerSource(filePath: string, content: string): void { sourceMapService.registerSource(filePath, content); } /** * Helper to add source mapping * @param sourceFilePath Path to the source file * @param sourceLine Line number in the source file * @param sourceColumn Column number in the source file * @param targetLine Line number in the combined file * @param targetColumn Column number in the combined file */ export function addMapping( sourceFilePath: string, sourceLine: number, sourceColumn: number, targetLine: number, targetColumn: number ): void { sourceMapService.addMapping( { filePath: sourceFilePath, line: sourceLine, column: sourceColumn }, targetLine, targetColumn ); } /** * Debug helper for CLI * @returns Debug information about all mappings */ export function getSourceMapDebugInfo(): string { return sourceMapService.getDebugInfo(); } // Enhanced debug helper that shows more detailed mapping information export function getDetailedSourceMapDebugInfo(): string { return sourceMapService.getDetailedDebugInfo(); } /** * Reset all source mappings (useful for tests) */ export function resetSourceMaps(): void { sourceMapService.reset(); }