mira-consciousness
Version:
Memory & Intelligence Retention Archive - Preserving The Spark
488 lines • 22.3 kB
JavaScript
/**
* 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