UNPKG

mira-consciousness

Version:

Memory & Intelligence Retention Archive - Preserving The Spark

488 lines 22.3 kB
/** * File System Analyzer for Cross-Platform Compatibility * Checks for file system related issues across different platforms */ import fs from 'fs-extra'; import * as path from 'path'; import { glob } from 'glob'; export class FileSystemAnalyzer { projectRoot; gitignorePatterns = []; constructor(projectRoot) { this.projectRoot = projectRoot; } async loadGitignorePatterns() { try { const gitignorePath = path.join(this.projectRoot, '.gitignore'); if (await fs.pathExists(gitignorePath)) { const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); this.gitignorePatterns = gitignoreContent .split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); } } catch { // Ignore errors } } async analyze(issues) { // Load gitignore patterns if not already loaded if (this.gitignorePatterns.length === 0) { await this.loadGitignorePatterns(); } const analysis = { pathSeparatorIssues: [], caseSensitivityIssues: [], reservedNameIssues: [], longPathIssues: [], permissionIssues: [] }; await Promise.all([ this.analyzePathHandling(issues, analysis), this.analyzeFilePermissions(issues, analysis), this.analyzeCaseSensitivity(issues, analysis), this.analyzeReservedNames(issues, analysis), this.analyzeFileSystemUsage(issues, analysis) ]); return analysis; } async analyzePathHandling(issues, analysis) { const pathPatterns = [ { pattern: /['"\/][A-Za-z]:[\\\/]/g, // Windows drive letters type: 'hardcoded_windows_path', message: 'Hardcoded Windows path detected', platforms: ['windows'], severity: 'high', recommendation: 'Use path.join() or path.resolve() for cross-platform paths' }, { pattern: /['"]\//g, // Unix absolute paths type: 'hardcoded_unix_path', message: 'Hardcoded Unix absolute path detected', platforms: ['linux', 'macOS', 'unix'], severity: 'medium', recommendation: 'Use path.join() or process.cwd() for portable paths' }, { // Match backslashes in path contexts, but not in regex literals or escape sequences // This pattern looks for backslashes that appear to be used as path separators pattern: /(?:['"`](?:[A-Za-z]:)?[^'"`]*?)\\(?![\\'"bfnrtv0-7xu]|$)/g, type: 'backslash_separator', message: 'Backslash path separator - may not work on Unix systems', platforms: ['windows'], severity: 'medium', recommendation: 'Use path.sep or path.join() for cross-platform compatibility' }, { pattern: /process\.cwd\(\)\s*\+\s*['"]/g, // String concatenation with cwd type: 'path_concatenation', message: 'Path concatenation with string - use path.join()', platforms: ['all'], severity: 'medium', recommendation: 'Replace with path.join(process.cwd(), ...)' }, { pattern: /__dirname\s*\+\s*['"]/g, // String concatenation with __dirname type: 'dirname_concatenation', message: '__dirname concatenation - use path.join()', platforms: ['all'], severity: 'medium', recommendation: 'Replace with path.join(__dirname, ...)' } ]; await this.scanWithPatterns(issues, pathPatterns, 'Path Handling', analysis); } async analyzeFilePermissions(issues, analysis) { const permissionPatterns = [ { pattern: /chmod\s*\(['"`]?[0-7]{3,4}/g, type: 'unix_permissions', message: 'Unix file permissions - not applicable on Windows', platforms: ['linux', 'macOS', 'unix'], severity: 'medium', recommendation: 'Check platform before setting permissions or use cross-platform library' }, { pattern: /fs\.chmod|fs\.chown|fs\.lchmod|fs\.lchown/g, type: 'permission_methods', message: 'File permission methods may fail on Windows', platforms: ['linux', 'macOS', 'unix'], severity: 'low', recommendation: 'Wrap in try-catch or check platform before use' }, { pattern: /umask\s*\(/g, type: 'umask_usage', message: 'umask usage - behavior differs across platforms', platforms: ['all'], severity: 'low', recommendation: 'Be aware of platform differences in umask behavior' } ]; await this.scanWithPatterns(issues, permissionPatterns, 'File Permissions', analysis); } async analyzeCaseSensitivity(issues, analysis) { try { const files = await glob('**/*.{js,ts,jsx,tsx}', { cwd: this.projectRoot, ignore: [ 'node_modules/**', '.git/**', 'dist/**', 'build/**', '**/coverage/**', '**/prettify.js', '**/*.min.js', '**/*.bundle.js' ] }); const fileMap = new Map(); for (const file of files) { const lowerFile = file.toLowerCase(); if (!fileMap.has(lowerFile)) { fileMap.set(lowerFile, []); } fileMap.get(lowerFile).push(file); } for (const [lowerFile, actualFiles] of fileMap.entries()) { if (actualFiles.length > 1) { issues.push({ file: actualFiles.join(', '), severity: 'high', category: 'File System', type: 'case_sensitivity', message: `Files differ only in case: ${actualFiles.join(', ')}`, affectedPlatforms: ['windows', 'macOS'], recommendation: 'Rename files to have unique names regardless of case', codeSnippet: actualFiles.join('\n') }); analysis.caseSensitivityIssues.push(actualFiles.join(', ')); } } // Check imports for case sensitivity issues for (const file of files) { const filePath = path.join(this.projectRoot, file); try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { const importMatch = line.match(/(?:import|require)\s*\(['"`]([^'"`]+)['"`]\)/); if (importMatch) { const importPath = importMatch[1]; if (importPath.includes('./') || importPath.includes('../')) { // Check if the actual file exists with different case const resolvedPath = path.resolve(path.dirname(filePath), importPath); if (resolvedPath.toLowerCase() !== resolvedPath) { analysis.caseSensitivityIssues.push(`Import with mixed case: ${importPath} in ${file}`); } } } }); } catch (error) { // Skip files that can't be read } } } catch (error) { // Skip if glob fails } } async analyzeReservedNames(issues, analysis) { const windowsReservedNames = [ 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9' ]; try { // Use the same ignore patterns as scanWithPatterns const ignorePatterns = [ '**/node_modules/**', '.git/**', '**/dist/**', '**/build/**', '**/coverage/**', '**/*.min.js', '**/*.bundle.js' ]; // Add gitignore patterns if (this.gitignorePatterns.length > 0) { const gitignoreGlobPatterns = this.gitignorePatterns.map(pattern => { if (pattern.startsWith('/')) pattern = pattern.slice(1); if (pattern.endsWith('/')) return '**/' + pattern.slice(0, -1) + '/**'; if (pattern.startsWith('*.')) return '**/' + pattern; if (!pattern.includes('/')) return '**/' + pattern; return pattern; }).filter(p => p); ignorePatterns.push(...gitignoreGlobPatterns); this.gitignorePatterns.forEach(pattern => { if (pattern.endsWith('/') && !pattern.startsWith('/')) { ignorePatterns.push(pattern.slice(0, -1) + '/**'); } }); } const files = await glob('**/*', { cwd: this.projectRoot, ignore: ignorePatterns, nodir: false }); for (const file of files) { const basename = path.basename(file).split('.')[0].toUpperCase(); if (windowsReservedNames.includes(basename)) { issues.push({ file, severity: 'critical', category: 'File System', type: 'reserved_name', message: `Windows reserved filename: ${basename}`, affectedPlatforms: ['windows'], recommendation: 'Rename to avoid Windows reserved names' }); analysis.reservedNameIssues.push(file); } // Check for problematic characters if (/[<>:"|?*]/.test(file)) { issues.push({ file, severity: 'high', category: 'File System', type: 'invalid_characters', message: 'Filename contains characters invalid on Windows', affectedPlatforms: ['windows'], recommendation: 'Remove characters: < > : " | ? *' }); } // Check for trailing dots or spaces (Windows strips these) if (/[\s.]$/.test(path.basename(file))) { issues.push({ file, severity: 'medium', category: 'File System', type: 'trailing_chars', message: 'Filename ends with space or dot - Windows will strip these', affectedPlatforms: ['windows'], recommendation: 'Remove trailing spaces and dots from filenames' }); } // Check for very long paths (Windows MAX_PATH is 260) const fullPath = path.join(this.projectRoot, file); if (fullPath.length > 250) { issues.push({ file, severity: 'medium', category: 'File System', type: 'long_path', message: `Path length (${fullPath.length}) approaches Windows MAX_PATH limit`, affectedPlatforms: ['windows'], recommendation: 'Consider shorter paths or enable long path support' }); analysis.longPathIssues.push(file); } } } catch (error) { // Skip if glob fails } } async analyzeFileSystemUsage(issues, analysis) { const fsPatterns = [ { pattern: /fs\.symlink|fs\.readlink/g, type: 'symlink_usage', message: 'Symbolic links - limited support on Windows', platforms: ['windows'], severity: 'medium', recommendation: 'Check for admin privileges on Windows or use alternatives' }, { pattern: /fs\.access.*fs\.constants\.X_OK/g, type: 'execute_permission', message: 'Execute permission check - behavior differs on Windows', platforms: ['windows'], severity: 'low', recommendation: 'Windows execute permissions work differently' }, { pattern: /\/dev\/null/g, type: 'dev_null', message: 'Unix /dev/null - use "nul" on Windows', platforms: ['linux', 'macOS', 'unix'], severity: 'medium', recommendation: 'Use os.devNull or check platform' }, { // Match /tmp when it appears to be used as a file path, not in URLs or comments pattern: /(?:['"`]|^|[\s(])\/tmp(?:\/|['"`\s)]|$)/g, type: 'tmp_directory', message: 'Unix /tmp directory - not available on Windows', platforms: ['linux', 'macOS', 'unix'], severity: 'medium', recommendation: 'Use os.tmpdir() for cross-platform temp directory' } ]; await this.scanWithPatterns(issues, fsPatterns, 'File System Usage', analysis); } isTestFile(filePath) { const testPatterns = [ /\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\/__tests__\//, /\/tests?\//, /\/e2e\//, /test-utils/, /test-helpers/, /\.mock\.[jt]sx?$/, /\/fixtures\// ]; return testPatterns.some(pattern => pattern.test(filePath)); } isExampleOrDemoFile(filePath) { const examplePatterns = [ /\/examples?\//, /\/demos?\//, /\.example\.[jt]sx?$/, /\/samples?\//, /\/docs?\//, /README/i ]; return examplePatterns.some(pattern => pattern.test(filePath)); } isAnalyzerFile(filePath) { return /\/analyzers?\//.test(filePath); } async scanWithPatterns(issues, patterns, category, analysis) { try { // Start with default ignores const ignorePatterns = [ '**/node_modules/**', '.git/**', '**/dist/**', '**/build/**', '**/coverage/**', '**/*.min.js', '**/*.bundle.js', '**/prettify.js' ]; // Add gitignore patterns if (this.gitignorePatterns.length > 0) { const gitignoreGlobPatterns = this.gitignorePatterns.map(pattern => { // Remove leading slash for glob if (pattern.startsWith('/')) { pattern = pattern.slice(1); } // Convert directory patterns if (pattern.endsWith('/')) { const dirName = pattern.slice(0, -1); return '**/' + dirName + '/**'; } // Add ** for extension patterns if (pattern.startsWith('*.')) { return '**/' + pattern; } // Handle paths that should match anywhere if (!pattern.includes('/')) { return '**/' + pattern; } return pattern; }).filter(p => p); ignorePatterns.push(...gitignoreGlobPatterns); // Also add root-level directory patterns this.gitignorePatterns.forEach(pattern => { if (pattern.endsWith('/') && !pattern.startsWith('/')) { const dirName = pattern.slice(0, -1); ignorePatterns.push(dirName + '/**'); } }); } const files = await glob('**/*.{js,ts,jsx,tsx}', { cwd: this.projectRoot, ignore: ignorePatterns }); for (const file of files) { const filePath = path.join(this.projectRoot, file); // Skip test files, examples, and analyzer files for certain checks const isTest = this.isTestFile(file); const isExample = this.isExampleOrDemoFile(file); const isAnalyzer = this.isAnalyzerFile(file); try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { // Skip lines that are likely comments or within regex literals const trimmedLine = line.trim(); if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*') || trimmedLine.startsWith('/*')) { return; // Skip comment lines } for (const pattern of patterns) { if (pattern.pattern.test(line)) { // Context-aware filtering let shouldReport = true; // In test files, only report if it's not an intentional test case if (isTest) { // Check if this is an intentional bad pattern for testing const prevLines = lines.slice(Math.max(0, index - 5), index); const nextLines = lines.slice(index + 1, Math.min(lines.length, index + 3)); const context = [...prevLines, line, ...nextLines].join('\n'); // Skip if it's clearly a test case demonstrating bad patterns if (context.includes('// File with') && context.includes('issues for testing') || context.includes('bad pattern') || context.includes('should fail') || context.includes('test case') || context.includes('intentional')) { shouldReport = false; } } // In analyzer files, skip if it's an example pattern if (isAnalyzer && (line.includes('bad:') || line.includes('pattern:') || line.includes('example:') || line.includes('// Example') || trimmedLine.startsWith('*'))) { shouldReport = false; } // In example/demo files, reduce severity let effectiveSeverity = pattern.severity; if (isExample && pattern.severity !== 'critical') { effectiveSeverity = 'low'; } if (shouldReport) { issues.push({ file, line: index + 1, severity: effectiveSeverity, category, type: pattern.type, message: pattern.message, affectedPlatforms: pattern.platforms, recommendation: pattern.recommendation, codeSnippet: line.trim() }); // Add to appropriate analysis category if (pattern.type.includes('path')) { analysis.pathSeparatorIssues.push(`${file}:${index + 1}`); } else if (pattern.type.includes('permission')) { analysis.permissionIssues.push(`${file}:${index + 1}`); } } } } }); } catch (error) { // Skip files that can't be read } } } catch (error) { // Skip if glob fails } } } //# sourceMappingURL=FileSystemAnalyzer.js.map