UNPKG

mushcode-mcp-server

Version:

A specialized Model Context Protocol server for MUSHCODE development assistance. Provides AI-powered code generation, validation, optimization, and examples for MUD development.

1,031 lines 39 kB
/** * MUSHCODE validation engine * Validates syntax, security, and best practices */ import { ValidationError } from '../utils/errors.js'; export class MushcodeValidator { knowledgeBase; constructor(knowledgeBase) { this.knowledgeBase = knowledgeBase; } /** * Validate MUSHCODE for syntax, security, and best practices */ async validate(request) { this.validateRequest(request); const lines = request.code.split('\n'); const context = this.createValidationContext(request, lines); // Perform all validation checks const syntaxErrors = await this.validateSyntax(context); const securityWarnings = request.checkSecurity ? await this.validateSecurity(context) : []; const bestPracticeSuggestions = request.checkBestPractices ? await this.validateBestPractices(context) : []; const compatibilityNotes = context.dialect ? this.checkCompatibility(context) : []; // Calculate quality scores const scores = this.calculateQualityScores(context, { syntaxErrors, securityWarnings, bestPracticeSuggestions }); const isValid = syntaxErrors.filter(e => e.severity === 'error').length === 0; return { isValid, syntaxErrors, securityWarnings, bestPracticeSuggestions, compatibilityNotes, totalLines: lines.length, ...scores }; } /** * Create validation context with all necessary information */ createValidationContext(request, lines) { const dialect = request.serverType ? this.knowledgeBase.dialects.get(request.serverType) || null : null; return { code: request.code, lines, dialect, strictMode: request.strictMode || false, checkSecurity: request.checkSecurity !== false, checkBestPractices: request.checkBestPractices !== false, serverType: request.serverType }; } /** * Validate the validation request */ validateRequest(request) { if (!request.code || request.code.trim().length === 0) { throw new ValidationError('Code is required'); } if (request.code.length > 50000) { throw new ValidationError('Code is too long (max 50000 characters)'); } if (request.serverType && !this.knowledgeBase.dialects.has(request.serverType)) { throw new ValidationError(`Unknown server type: ${request.serverType}`); } } /** * Validate MUSHCODE syntax */ async validateSyntax(context) { const errors = []; const validators = [ this.validateBasicSyntax, this.validateBracketMatching, this.validateFunctionSyntax, this.validateAttributeSyntax, this.validateCommandSyntax, this.validateStringLiterals, this.validateRegexPatterns, this.validateVariableReferences ]; // Run line-by-line validation for (let i = 0; i < context.lines.length; i++) { const line = context.lines[i]; if (!line) continue; const lineNumber = i + 1; const trimmedLine = line.trim(); // Skip empty lines and comments if (trimmedLine.length === 0 || trimmedLine.startsWith('@@')) { continue; } const lineContext = { ...context, line, lineNumber, trimmedLine }; // Run all line validators for (const validator of validators) { errors.push(...validator.call(this, lineContext)); } } // Check for global syntax issues errors.push(...this.validateGlobalSyntax(context)); return errors; } /** * Validate basic syntax rules */ validateBasicSyntax(context) { const errors = []; const { line, lineNumber, strictMode } = context; // Check for invalid characters errors.push(...this.checkInvalidCharacters(line, lineNumber)); // Check for excessive line length in strict mode if (strictMode && line.length > 200) { errors.push({ line: lineNumber, column: 201, message: 'Line too long (>200 characters)', severity: 'warning', code: 'LINE_TOO_LONG', suggestion: 'Break long lines for better readability', fixable: false }); } // Check for trailing whitespace if (line.endsWith(' ') || line.endsWith('\t')) { errors.push({ line: lineNumber, column: line.length, message: 'Trailing whitespace', severity: 'info', code: 'TRAILING_WHITESPACE', suggestion: 'Remove trailing whitespace', fixable: true }); } return errors; } /** * Check for invalid characters */ checkInvalidCharacters(line, lineNumber) { const errors = []; // Allow Unicode characters - only flag actual control characters const invalidChars = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g; let match; while ((match = invalidChars.exec(line)) !== null) { errors.push({ line: lineNumber, column: match.index + 1, message: `Invalid control character: ${match[0].charCodeAt(0).toString(16)}`, severity: 'error', code: 'INVALID_CHAR', suggestion: 'Remove or replace invalid characters', fixable: true }); } return errors; } /** * Validate bracket and parentheses matching */ validateBracketMatching(context) { const errors = []; const { line, lineNumber } = context; const stack = []; const pairs = { '(': ')', '[': ']', '{': '}' }; // Track brackets while respecting string literals let inString = false; let escapeNext = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '"') { inString = !inString; continue; } // Skip bracket checking inside strings if (inString) continue; if (char && Object.keys(pairs).includes(char)) { stack.push({ char, pos: i }); } else if (char && Object.values(pairs).includes(char)) { if (stack.length === 0) { errors.push({ line: lineNumber, column: i + 1, message: `Unmatched closing ${char}`, severity: 'error', code: 'UNMATCHED_BRACKET', suggestion: `Add opening ${Object.keys(pairs).find(k => pairs[k] === char)}`, fixable: true }); } else { const last = stack.pop(); if (last && pairs[last.char] !== char) { errors.push({ line: lineNumber, column: i + 1, message: `Mismatched brackets: expected ${pairs[last.char]}, got ${char}`, severity: 'error', code: 'MISMATCHED_BRACKET', suggestion: `Change ${char} to ${pairs[last.char]}`, fixable: true }); } } } } // Check for unclosed brackets for (const item of stack) { errors.push({ line: lineNumber, column: item.pos + 1, message: `Unclosed ${item.char}`, severity: 'error', code: 'UNCLOSED_BRACKET', suggestion: `Add closing ${pairs[item.char]}`, fixable: true }); } return errors; } /** * Validate function syntax */ validateFunctionSyntax(context) { const errors = []; const { line, lineNumber, dialect } = context; // Match function calls: functionname(args) const functionPattern = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g; let match; while ((match = functionPattern.exec(line)) !== null) { const functionName = match[1]; if (!functionName) continue; // Check if function exists in dialect if (dialect) { const func = dialect.functionLibrary.find(f => f.name.toLowerCase() === functionName.toLowerCase()); if (!func) { errors.push({ line: lineNumber, column: match.index + 1, message: `Unknown function: ${functionName}`, severity: 'warning', code: 'UNKNOWN_FUNCTION', suggestion: 'Check function name spelling or server compatibility', fixable: false }); } else if (func.deprecated) { errors.push({ line: lineNumber, column: match.index + 1, message: `Deprecated function: ${functionName}`, severity: 'warning', code: 'DEPRECATED_FUNCTION', suggestion: func.alternativeTo ? `Use ${func.alternativeTo} instead` : 'Consider using alternative function', fixable: false }); } } // Check for common function syntax errors const afterParen = line.substring(match.index + match[0].length); if (afterParen.startsWith(' ')) { errors.push({ line: lineNumber, column: match.index + match[0].length + 1, message: 'Unexpected space after opening parenthesis', severity: 'warning', code: 'SPACE_AFTER_PAREN', suggestion: 'Remove space after opening parenthesis', fixable: true }); } } return errors; } /** * Validate attribute syntax */ validateAttributeSyntax(context) { const errors = []; const { line, lineNumber } = context; // Match attribute references: &ATTR (more flexible pattern) const attrPattern = /&([a-zA-Z_][a-zA-Z0-9_-]*)/g; let match; while ((match = attrPattern.exec(line)) !== null) { const attrName = match[1]; if (!attrName) continue; // Check attribute naming conventions if (attrName.length > 32) { errors.push({ line: lineNumber, column: match.index + 1, message: `Attribute name too long: ${attrName} (max 32 characters)`, severity: 'error', code: 'ATTR_NAME_TOO_LONG', suggestion: 'Shorten attribute name', fixable: false }); } // Check for invalid characters in attribute names if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(attrName)) { errors.push({ line: lineNumber, column: match.index + 1, message: `Invalid attribute name: ${attrName}`, severity: 'error', code: 'INVALID_ATTR_NAME', suggestion: 'Use only letters, numbers, and underscores', fixable: false }); } } return errors; } /** * Validate string literals */ validateStringLiterals(context) { const errors = []; const { line, lineNumber } = context; // Check for unterminated strings let inString = false; let escapeNext = false; let stringStart = -1; for (let i = 0; i < line.length; i++) { const char = line[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '"') { if (!inString) { inString = true; stringStart = i; } else { inString = false; stringStart = -1; } } } // Check for unterminated string if (inString && stringStart >= 0) { errors.push({ line: lineNumber, column: stringStart + 1, message: 'Unterminated string literal', severity: 'error', code: 'UNTERMINATED_STRING', suggestion: 'Add closing quote', fixable: true }); } return errors; } /** * Validate regex patterns */ validateRegexPatterns(context) { const errors = []; const { line, lineNumber } = context; // Check for regex functions with potentially invalid patterns const regexFunctions = ['regedit', 'regmatch', 'regsub']; for (const func of regexFunctions) { const pattern = new RegExp(`\\b${func}\\s*\\(([^,]+),\\s*([^,)]+)`, 'g'); let match; while ((match = pattern.exec(line)) !== null) { const regexPattern = match[2]?.trim(); if (regexPattern && regexPattern.startsWith('"') && regexPattern.endsWith('"')) { const regex = regexPattern.slice(1, -1); try { new RegExp(regex); } catch (e) { errors.push({ line: lineNumber, column: match.index + match[0].indexOf(regexPattern) + 1, message: `Invalid regex pattern: ${e instanceof Error ? e.message : 'Unknown error'}`, severity: 'error', code: 'INVALID_REGEX', suggestion: 'Fix regex pattern syntax', fixable: false }); } } } } return errors; } /** * Validate variable references */ validateVariableReferences(context) { const errors = []; const { line, lineNumber } = context; // Check for invalid variable references const varPattern = /%([0-9]+|[a-zA-Z_][a-zA-Z0-9_]*)/g; let match; while ((match = varPattern.exec(line)) !== null) { const varName = match[1]; if (!varName) continue; // Check for numeric variables that are too high if (/^\d+$/.test(varName)) { const num = parseInt(varName, 10); if (num > 99) { errors.push({ line: lineNumber, column: match.index + 1, message: `Variable number too high: %${varName} (max %99)`, severity: 'warning', code: 'VAR_NUMBER_TOO_HIGH', suggestion: 'Use variables %0-%99 or named variables', fixable: false }); } } // Check for invalid variable names if (!/^[0-9]+$/.test(varName) && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(varName)) { errors.push({ line: lineNumber, column: match.index + 1, message: `Invalid variable name: %${varName}`, severity: 'error', code: 'INVALID_VAR_NAME', suggestion: 'Use alphanumeric characters and underscores only', fixable: false }); } } return errors; } /** * Validate command syntax */ validateCommandSyntax(context) { const errors = []; const { line, lineNumber, trimmedLine } = context; // Match commands starting with @ const commandPattern = /^@([a-zA-Z_][a-zA-Z0-9_]*)/; const match = commandPattern.exec(trimmedLine); if (match) { const commandName = match[1]; if (!commandName) return errors; // Check for common command syntax issues if (line.includes(' ')) { errors.push({ line: lineNumber, column: line.indexOf(' ') + 1, message: 'Multiple consecutive spaces', severity: 'info', code: 'MULTIPLE_SPACES', suggestion: 'Use single spaces for better readability', fixable: true }); } // Check for missing semicolon at end of command if (!trimmedLine.endsWith(';') && !trimmedLine.endsWith('}')) { errors.push({ line: lineNumber, column: line.length + 1, message: 'Command should end with semicolon', severity: 'info', code: 'MISSING_SEMICOLON', suggestion: 'Add semicolon at end of command', fixable: true }); } // Check for invalid command names if (commandName.length > 32) { errors.push({ line: lineNumber, column: match.index + 1, message: `Command name too long: ${commandName} (max 32 characters)`, severity: 'warning', code: 'COMMAND_NAME_TOO_LONG', suggestion: 'Shorten command name', fixable: false }); } } return errors; } /** * Validate global syntax issues */ validateGlobalSyntax(context) { const errors = []; const { code } = context; // Check for overall bracket balance const brackets = { '(': 0, '[': 0, '{': 0 }; const pairs = { '(': ')', '[': ']', '{': '}' }; let inString = false; let escapeNext = false; for (const char of code) { if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '"') { inString = !inString; continue; } // Skip bracket checking inside strings if (inString) continue; if (char && Object.keys(brackets).includes(char)) { brackets[char]++; } else if (char && Object.values(pairs).includes(char)) { const openChar = Object.keys(pairs).find(k => pairs[k] === char); if (openChar && brackets[openChar] > 0) { brackets[openChar]--; } } } for (const [bracket, count] of Object.entries(brackets)) { if (count > 0) { errors.push({ line: 1, column: 1, message: `${count} unclosed ${bracket} bracket(s)`, severity: 'error', code: 'GLOBAL_UNCLOSED_BRACKET', suggestion: `Add ${count} closing ${pairs[bracket]} bracket(s)`, fixable: true }); } } // Check for excessive nesting depth const maxDepth = this.calculateMaxNestingDepth(code); if (maxDepth > 10) { errors.push({ line: 1, column: 1, message: `Excessive nesting depth: ${maxDepth} levels`, severity: 'warning', code: 'EXCESSIVE_NESTING', suggestion: 'Consider breaking complex expressions into smaller parts', fixable: false }); } return errors; } /** * Calculate maximum nesting depth */ calculateMaxNestingDepth(code) { let maxDepth = 0; let currentDepth = 0; let inString = false; let escapeNext = false; for (const char of code) { if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '"') { inString = !inString; continue; } if (inString) continue; if (char === '(' || char === '[' || char === '{') { currentDepth++; maxDepth = Math.max(maxDepth, currentDepth); } else if (char === ')' || char === ']' || char === '}') { currentDepth = Math.max(0, currentDepth - 1); } } return maxDepth; } /** * Validate security vulnerabilities */ async validateSecurity(context) { const warnings = []; const { code, lines, dialect } = context; // Get all security rules const securityRules = Array.from(this.knowledgeBase.securityRules.values()); for (const rule of securityRules) { // Skip rules not applicable to current server if (dialect && !rule.affectedServers.includes(dialect.name)) { continue; } try { const pattern = new RegExp(rule.pattern, 'gi'); let match; while ((match = pattern.exec(code)) !== null) { const lineNumber = this.getLineNumber(code, match.index); const columnNumber = this.getColumnNumber(code, match.index); warnings.push({ ruleId: rule.ruleId, type: rule.category, description: rule.description, lineNumber, columnNumber, severity: rule.severity, mitigation: rule.recommendation, codeSnippet: this.getCodeSnippet(lines, lineNumber), references: rule.references }); } } catch (error) { // Log invalid regex patterns but don't fail validation console.warn(`Invalid regex pattern in security rule ${rule.ruleId}: ${rule.pattern}`); } } // Add additional security checks warnings.push(...this.checkCommonSecurityIssues(context)); return warnings; } /** * Check for common security issues not covered by rules */ checkCommonSecurityIssues(context) { const warnings = []; const { code, lines } = context; // Check for hardcoded passwords or sensitive data const sensitivePatterns = [ { pattern: /password\s*=\s*["']([^"']+)["']/gi, type: 'hardcoded_password' }, { pattern: /secret\s*=\s*["']([^"']+)["']/gi, type: 'hardcoded_secret' }, { pattern: /api[_-]?key\s*=\s*["']([^"']+)["']/gi, type: 'hardcoded_api_key' } ]; for (const { pattern, type } of sensitivePatterns) { let match; while ((match = pattern.exec(code)) !== null) { const lineNumber = this.getLineNumber(code, match.index); const columnNumber = this.getColumnNumber(code, match.index); warnings.push({ ruleId: `BUILTIN_${type.toUpperCase()}`, type: 'data', description: `Potential hardcoded sensitive data: ${type.replace('_', ' ')}`, lineNumber, columnNumber, severity: 'medium', mitigation: 'Use configuration files or environment variables for sensitive data', codeSnippet: this.getCodeSnippet(lines, lineNumber), references: [] }); } } return warnings; } /** * Validate best practices */ async validateBestPractices(context) { const improvements = []; const { lines } = context; // Check for common best practice violations improvements.push(...this.checkCodeStyle(lines)); improvements.push(...this.checkPerformance(lines)); improvements.push(...this.checkMaintainability(lines)); improvements.push(...this.checkReadability(lines)); return improvements; } /** * Check code style best practices */ checkCodeStyle(lines) { const improvements = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) continue; const lineNumber = i + 1; // Check for inconsistent indentation (not using 2 spaces) const indentMatch = line.match(/^(\s+)/); if (indentMatch && indentMatch[1]) { const indent = indentMatch[1]; // Check if indentation is not multiples of 2 spaces if (indent.includes('\t') || (indent.length % 2 !== 0)) { improvements.push({ type: 'readability', description: 'Inconsistent indentation', lineNumber, before: line, after: line.replace(/^\s+/, ' '.repeat(Math.ceil(indent.length / 2))), impact: 'Improves code readability and consistency', confidence: 0.8, effort: 'low', category: 'formatting' }); } } // Check for missing comments on complex lines if (line.includes('switch(') && !line.includes('@@') && !lines[i - 1]?.includes('@@')) { improvements.push({ type: 'readability', description: 'Complex logic without comments', lineNumber, before: line, after: `@@ Conditional logic\n${line}`, impact: 'Makes code easier to understand and maintain', confidence: 0.6, effort: 'low', category: 'documentation' }); } } return improvements; } /** * Check performance best practices */ checkPerformance(lines) { const improvements = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) continue; const lineNumber = i + 1; // Check for inefficient nested loops if (line.includes('iter(') && line.includes('iter(')) { improvements.push({ type: 'performance', description: 'Nested iter() functions can be slow', lineNumber, before: line, after: line, // Would need more context for actual improvement impact: 'Reduces execution time for large datasets', confidence: 0.7, effort: 'medium', category: 'optimization' }); } // Check for repeated expensive operations const expensiveOps = ['sql(', 'search(', 'lsearch(']; for (const op of expensiveOps) { const count = (line.match(new RegExp(op.replace('(', '\\('), 'g')) || []).length; if (count > 1) { improvements.push({ type: 'performance', description: `Multiple ${op} calls in single line`, lineNumber, before: line, after: line, // Would need more context for actual improvement impact: 'Cache results to avoid repeated expensive operations', confidence: 0.8, effort: 'medium', category: 'caching' }); } } } return improvements; } /** * Check maintainability best practices */ checkMaintainability(lines) { const improvements = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) continue; const lineNumber = i + 1; // Check for magic numbers const magicNumbers = line.match(/\b\d{2,}\b/g); if (magicNumbers) { for (const num of magicNumbers) { if (parseInt(num) > 10) { // Ignore small numbers improvements.push({ type: 'maintainability', description: `Magic number: ${num}`, lineNumber, before: line, after: line.replace(num, `[v(CONSTANT_${num})]`), impact: 'Makes code more maintainable by using named constants', confidence: 0.6, effort: 'low', category: 'constants' }); } } } // Check for long parameter lists const paramCount = (line.match(/%\d/g) || []).length; if (paramCount > 5) { improvements.push({ type: 'maintainability', description: 'Too many parameters', lineNumber, before: line, after: line, // Would need restructuring impact: 'Consider using a data structure or breaking into smaller functions', confidence: 0.7, effort: 'high', category: 'structure' }); } } return improvements; } /** * Check readability best practices */ checkReadability(lines) { const improvements = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) continue; const lineNumber = i + 1; // Check for overly complex expressions const complexity = this.calculateLineComplexity(line); if (complexity > 10) { improvements.push({ type: 'readability', description: 'Complex expression', lineNumber, before: line, after: line, // Would need breaking down impact: 'Break complex expressions into smaller, more readable parts', confidence: 0.8, effort: 'medium', category: 'complexity' }); } // Check for unclear variable names const registers = line.match(/q\d+/g); if (registers && registers.length > 3) { improvements.push({ type: 'readability', description: 'Many register variables', lineNumber, before: line, after: line, // Would need better naming impact: 'Use descriptive register names or comments to clarify purpose', confidence: 0.6, effort: 'low', category: 'naming' }); } } return improvements; } /** * Check server compatibility */ checkCompatibility(context) { const notes = []; const { code, dialect } = context; if (!dialect) return notes; // Check for server-specific functions for (const func of dialect.functionLibrary) { if (func.name && code.includes(func.name) && func.notes) { for (const note of func.notes) { if (note.includes('compatibility') || note.includes('version')) { notes.push(`${func.name}: ${note}`); } } } } // Check for deprecated features for (const func of dialect.functionLibrary) { if (func.deprecated && func.name && code.includes(func.name)) { notes.push(`${func.name} is deprecated in ${dialect.name}`); if (func.alternativeTo) { notes.push(`Consider using ${func.alternativeTo} instead`); } } } return notes; } /** * Calculate quality scores for the code */ calculateQualityScores(context, results) { return { complexityScore: this.calculateComplexityScore(context), securityScore: this.calculateSecurityScore(results.securityWarnings), maintainabilityScore: this.calculateMaintainabilityScore(context, results.bestPracticeSuggestions) }; } /** * Calculate complexity score (0-100) */ calculateComplexityScore(context) { const { code, lines } = context; let complexity = 0; // Base complexity from line count complexity += Math.min(lines.length * 0.5, 20); // Add complexity for control structures const controlStructures = ['switch(', 'iter(', 'fold(', 'filter(', 'map(', 'select(']; for (const structure of controlStructures) { const count = (code.match(new RegExp(structure.replace('(', '\\('), 'g')) || []).length; complexity += count * 2; } // Add complexity for nesting depth const maxDepth = this.calculateMaxNestingDepth(code); complexity += maxDepth * 3; // Add complexity for function calls const functionCalls = (code.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\s*\(/g) || []).length; complexity += functionCalls * 0.5; return Math.min(Math.round(complexity), 100); } /** * Calculate security score (0-100, higher is better) */ calculateSecurityScore(warnings) { if (warnings.length === 0) return 100; let deductions = 0; const severityWeights = { critical: 30, high: 20, medium: 10, low: 5 }; for (const warning of warnings) { deductions += severityWeights[warning.severity] || 5; } return Math.max(0, 100 - deductions); } /** * Calculate maintainability score (0-100) */ calculateMaintainabilityScore(context, suggestions) { const { lines } = context; let score = 100; // Deduct for length if (lines.length > 50) score -= 10; if (lines.length > 100) score -= 20; if (lines.length > 200) score -= 30; // Deduct for lack of comments const commentLines = lines.filter(line => line.trim().startsWith('@@')).length; const commentRatio = lines.length > 0 ? commentLines / lines.length : 0; if (commentRatio < 0.1) score -= 15; if (commentRatio < 0.05) score -= 10; // Deduct for suggestions const suggestionWeights = { maintainability: 5, readability: 3, performance: 4, security: 6, best_practice: 2 }; for (const suggestion of suggestions) { score -= suggestionWeights[suggestion.type] || 1; } // Bonus for good practices if (commentRatio > 0.2) score += 5; if (lines.length > 0 && lines.length < 50) score += 5; return Math.max(0, Math.min(100, Math.round(score))); } /** * Calculate line complexity */ calculateLineComplexity(line) { let complexity = 1; // Count operators and functions const operators = ['switch(', 'if(', 'iter(', 'fold(', 'filter(', 'map(']; for (const op of operators) { complexity += (line.match(new RegExp(op.replace('(', '\\('), 'g')) || []).length; } // Count nesting let depth = 0; for (const char of line) { if (char === '(' || char === '[') depth++; } complexity += Math.floor(depth / 2); return complexity; } /** * Get line number from character index */ getLineNumber(code, index) { return code.substring(0, index).split('\n').length; } /** * Get column number from character index */ getColumnNumber(code, index) { const lines = code.substring(0, index).split('\n'); return lines[lines.length - 1]?.length ?? 0 + 1; } /** * Get code snippet around a line */ getCodeSnippet(lines, lineNumber) { const start = Math.max(0, lineNumber - 2); const end = Math.min(lines.length, lineNumber + 1); return lines.slice(start, end).join('\n'); } } //# sourceMappingURL=validator.js.map