meld
Version:
Meld: A template language for LLM prompts
234 lines (203 loc) • 8.2 kB
text/typescript
/**
* 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();
}