codesummary 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/scanner.js CHANGED
@@ -1,330 +1,468 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import chalk from 'chalk';
4
- import ErrorHandler from './errorHandler.js';
5
-
6
- /**
7
- * File Scanner for CodeSummary
8
- * Handles recursive directory traversal and file filtering
9
- */
10
- export class Scanner {
11
- constructor(config) {
12
- this.config = config;
13
- this.allowedExtensions = new Set(config.allowedExtensions.map(ext => ext.toLowerCase()));
14
- this.excludeDirs = new Set(config.excludeDirs);
15
- }
16
-
17
- /**
18
- * Scan a directory recursively and return files grouped by extension
19
- * @param {string} rootPath - Root directory to scan
20
- * @returns {Promise<object>} Object with extensions as keys and file arrays as values
21
- */
22
- async scanDirectory(rootPath) {
23
- try {
24
- // Validate and sanitize input path
25
- const sanitizedPath = ErrorHandler.sanitizeInput(rootPath);
26
- ErrorHandler.validatePath(sanitizedPath, { mustExist: true, preventTraversal: true });
27
-
28
- const resolvedRoot = path.resolve(sanitizedPath);
29
- const stats = await fs.stat(resolvedRoot);
30
-
31
- if (!stats.isDirectory()) {
32
- throw new Error(`Path is not a directory: ${resolvedRoot}`);
33
- }
34
-
35
- console.log(chalk.gray(`Scanning directory: ${resolvedRoot}`));
36
-
37
- const filesByExtension = {};
38
- const scannedFiles = new Set(); // Prevent duplicates
39
-
40
- await this.walkDirectory(resolvedRoot, resolvedRoot, filesByExtension, scannedFiles);
41
-
42
- // Sort files within each extension group
43
- Object.keys(filesByExtension).forEach(ext => {
44
- filesByExtension[ext].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
45
- });
46
-
47
- return filesByExtension;
48
- } catch (error) {
49
- if (error.code === 'ENOENT') {
50
- throw new Error(`Directory does not exist: ${rootPath}`);
51
- } else if (error.code === 'EACCES') {
52
- throw new Error(`Permission denied accessing directory: ${rootPath}`);
53
- }
54
- throw error;
55
- }
56
- }
57
-
58
- /**
59
- * Recursively walk through directory structure
60
- * @param {string} currentPath - Current directory being processed
61
- * @param {string} rootPath - Original root path for relative path calculation
62
- * @param {object} filesByExtension - Accumulator object for results
63
- * @param {Set} scannedFiles - Set to track processed files and avoid duplicates
64
- */
65
- async walkDirectory(currentPath, rootPath, filesByExtension, scannedFiles) {
66
- try {
67
- const entries = await fs.readdir(currentPath, { withFileTypes: true });
68
-
69
- for (const entry of entries) {
70
- const fullPath = path.join(currentPath, entry.name);
71
- const relativePath = path.relative(rootPath, fullPath);
72
-
73
- if (entry.isDirectory()) {
74
- // Skip excluded directories and hidden directories (unless explicitly allowed)
75
- if (this.shouldSkipDirectory(entry.name, relativePath)) {
76
- continue;
77
- }
78
-
79
- // Recursively scan subdirectory
80
- await this.walkDirectory(fullPath, rootPath, filesByExtension, scannedFiles);
81
- } else if (entry.isFile()) {
82
- // Process file if it matches criteria
83
- await this.processFile(fullPath, rootPath, filesByExtension, scannedFiles);
84
- }
85
- // Skip symbolic links, special files, etc.
86
- }
87
- } catch (error) {
88
- // Handle different types of file system errors appropriately
89
- if (error.code === 'EACCES' || error.code === 'EPERM') {
90
- console.warn(chalk.yellow(`WARNING: Permission denied: ${currentPath}`));
91
- } else if (error.code === 'ENOENT') {
92
- console.warn(chalk.yellow(`WARNING: Directory no longer exists: ${currentPath}`));
93
- } else {
94
- console.warn(chalk.yellow(`WARNING: Cannot read directory ${currentPath}: ${error.message}`));
95
- }
96
- }
97
- }
98
-
99
- /**
100
- * Process a single file and add it to results if it matches criteria
101
- * @param {string} fullPath - Absolute path to the file
102
- * @param {string} rootPath - Root path for relative calculation
103
- * @param {object} filesByExtension - Results accumulator
104
- * @param {Set} scannedFiles - Set of already processed files
105
- */
106
- async processFile(fullPath, rootPath, filesByExtension, scannedFiles) {
107
- try {
108
- const relativePath = path.relative(rootPath, fullPath);
109
-
110
- // Avoid processing the same file twice (in case of symlinks)
111
- if (scannedFiles.has(fullPath)) {
112
- return;
113
- }
114
- scannedFiles.add(fullPath);
115
-
116
- const extension = path.extname(relativePath).toLowerCase();
117
-
118
- // Skip files without extensions or not in allowed list
119
- if (!extension || !this.allowedExtensions.has(extension)) {
120
- return;
121
- }
122
-
123
- // Skip hidden files (starting with .) unless explicitly needed
124
- const fileName = path.basename(relativePath);
125
- if (fileName.startsWith('.') && !this.isAllowedHiddenFile(fileName)) {
126
- return;
127
- }
128
-
129
- // Verify file is readable
130
- const stats = await fs.stat(fullPath);
131
- if (!stats.isFile()) {
132
- return;
133
- }
134
-
135
- // Add to results
136
- if (!filesByExtension[extension]) {
137
- filesByExtension[extension] = [];
138
- }
139
-
140
- filesByExtension[extension].push({
141
- relativePath: relativePath.replace(/\\/g, '/'), // Normalize path separators
142
- absolutePath: fullPath,
143
- size: stats.size,
144
- modified: stats.mtime
145
- });
146
-
147
- } catch (error) {
148
- // Handle file processing errors with appropriate context
149
- if (error.code === 'EACCES' || error.code === 'EPERM') {
150
- console.warn(chalk.yellow(`WARNING: Permission denied: ${relativePath}`));
151
- } else if (error.code === 'ENOENT') {
152
- // File might have been deleted during scan
153
- console.warn(chalk.yellow(`WARNING: File no longer exists: ${relativePath}`));
154
- } else {
155
- console.warn(chalk.yellow(`WARNING: Cannot process file ${relativePath}: ${error.message}`));
156
- }
157
- }
158
- }
159
-
160
- /**
161
- * Determine if a directory should be skipped
162
- * @param {string} dirName - Directory name
163
- * @param {string} relativePath - Relative path from root
164
- * @returns {boolean} True if directory should be skipped
165
- */
166
- shouldSkipDirectory(dirName, relativePath) {
167
- // Skip directories in exclude list
168
- if (this.excludeDirs.has(dirName)) {
169
- return true;
170
- }
171
-
172
- // Skip hidden directories (starting with .) unless explicitly allowed
173
- if (dirName.startsWith('.') && !this.isAllowedHiddenDirectory(dirName)) {
174
- return true;
175
- }
176
-
177
- // Skip common build/cache directories that might not be in exclude list
178
- const commonSkipDirs = new Set([
179
- 'tmp', 'temp', 'cache', '.cache', 'logs', '.logs',
180
- 'bower_components', 'vendor', '.vendor'
181
- ]);
182
-
183
- if (commonSkipDirs.has(dirName.toLowerCase())) {
184
- return true;
185
- }
186
-
187
- return false;
188
- }
189
-
190
- /**
191
- * Check if a hidden file should be included
192
- * @param {string} fileName - File name
193
- * @returns {boolean} True if file should be included
194
- */
195
- isAllowedHiddenFile(fileName) {
196
- const allowedHiddenFiles = new Set([
197
- '.gitignore', '.gitattributes', '.editorconfig',
198
- '.eslintrc.js', '.eslintrc.json', '.prettierrc',
199
- '.env.example', '.htaccess'
200
- ]);
201
-
202
- return allowedHiddenFiles.has(fileName);
203
- }
204
-
205
- /**
206
- * Check if a hidden directory should be included
207
- * @param {string} dirName - Directory name
208
- * @returns {boolean} True if directory should be included
209
- */
210
- isAllowedHiddenDirectory(dirName) {
211
- const allowedHiddenDirs = new Set([
212
- '.github', '.gitlab', '.circleci'
213
- ]);
214
-
215
- return allowedHiddenDirs.has(dirName);
216
- }
217
-
218
- /**
219
- * Get file extension descriptions for user display
220
- * @param {object} filesByExtension - Files grouped by extension
221
- * @returns {Array} Array of extension info objects
222
- */
223
- getExtensionInfo(filesByExtension) {
224
- const extensionDescriptions = {
225
- '.js': 'JavaScript',
226
- '.ts': 'TypeScript',
227
- '.jsx': 'React JSX',
228
- '.tsx': 'TypeScript JSX',
229
- '.json': 'JSON',
230
- '.xml': 'XML',
231
- '.html': 'HTML',
232
- '.css': 'CSS',
233
- '.scss': 'SCSS',
234
- '.sass': 'Sass',
235
- '.md': 'Markdown',
236
- '.txt': 'Text',
237
- '.py': 'Python',
238
- '.java': 'Java',
239
- '.cs': 'C#',
240
- '.cpp': 'C++',
241
- '.c': 'C',
242
- '.h': 'Header',
243
- '.yaml': 'YAML',
244
- '.yml': 'YAML',
245
- '.sh': 'Shell Script',
246
- '.bat': 'Batch File',
247
- '.ps1': 'PowerShell',
248
- '.php': 'PHP',
249
- '.rb': 'Ruby',
250
- '.go': 'Go',
251
- '.rs': 'Rust',
252
- '.swift': 'Swift',
253
- '.kt': 'Kotlin',
254
- '.scala': 'Scala',
255
- '.vue': 'Vue.js',
256
- '.svelte': 'Svelte',
257
- '.dockerfile': 'Dockerfile',
258
- '.sql': 'SQL',
259
- '.graphql': 'GraphQL'
260
- };
261
-
262
- return Object.keys(filesByExtension)
263
- .sort()
264
- .map(ext => ({
265
- extension: ext,
266
- description: extensionDescriptions[ext] || 'Unknown',
267
- count: filesByExtension[ext].length,
268
- files: filesByExtension[ext]
269
- }));
270
- }
271
-
272
- /**
273
- * Calculate total statistics for scanned files
274
- * @param {object} filesByExtension - Files grouped by extension
275
- * @returns {object} Statistics object
276
- */
277
- calculateStatistics(filesByExtension) {
278
- let totalFiles = 0;
279
- let totalSize = 0;
280
- const extensionCount = Object.keys(filesByExtension).length;
281
-
282
- Object.values(filesByExtension).forEach(files => {
283
- totalFiles += files.length;
284
- totalSize += files.reduce((sum, file) => sum + file.size, 0);
285
- });
286
-
287
- return {
288
- totalFiles,
289
- totalSize,
290
- extensionCount,
291
- averageFileSize: totalFiles > 0 ? Math.round(totalSize / totalFiles) : 0,
292
- totalSizeFormatted: this.formatFileSize(totalSize)
293
- };
294
- }
295
-
296
- /**
297
- * Format file size in human readable format
298
- * @param {number} bytes - Size in bytes
299
- * @returns {string} Formatted size string
300
- */
301
- formatFileSize(bytes) {
302
- const units = ['B', 'KB', 'MB', 'GB'];
303
- let size = bytes;
304
- let unitIndex = 0;
305
-
306
- while (size >= 1024 && unitIndex < units.length - 1) {
307
- size /= 1024;
308
- unitIndex++;
309
- }
310
-
311
- return `${size.toFixed(1)} ${units[unitIndex]}`;
312
- }
313
-
314
- /**
315
- * Display scan results summary
316
- * @param {object} filesByExtension - Files grouped by extension
317
- */
318
- displayScanSummary(filesByExtension) {
319
- const stats = this.calculateStatistics(filesByExtension);
320
- const extensions = Object.keys(filesByExtension).sort();
321
-
322
- console.log(chalk.cyan('\nScan Summary:'));
323
- console.log(chalk.gray(` Extensions found: ${extensions.join(', ')}`));
324
- console.log(chalk.gray(` Total files: ${stats.totalFiles}`));
325
- console.log(chalk.gray(` Total size: ${stats.totalSizeFormatted}`));
326
- console.log();
327
- }
328
- }
329
-
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import ErrorHandler from './errorHandler.js';
5
+
6
+ /**
7
+ * File Scanner for CodeSummary
8
+ * Handles recursive directory traversal and file filtering
9
+ */
10
+ export class Scanner {
11
+ constructor(config) {
12
+ this.config = config;
13
+ this.allowedExtensions = new Set(config.allowedExtensions.map(ext => ext.toLowerCase()));
14
+ this.excludeDirs = new Set(config.excludeDirs);
15
+ this.excludeFiles = config.excludeFiles || [];
16
+ }
17
+
18
+ /**
19
+ * Scan a directory recursively and return files grouped by extension
20
+ * @param {string} rootPath - Root directory to scan
21
+ * @returns {Promise<object>} Object with extensions as keys and file arrays as values
22
+ */
23
+ async scanDirectory(rootPath) {
24
+ const scanErrors = [];
25
+ const scanWarnings = [];
26
+
27
+ try {
28
+ // For scanner paths, we only need basic validation (no aggressive sanitization)
29
+ if (!rootPath || typeof rootPath !== 'string') {
30
+ throw new Error('Invalid root path: must be a non-empty string');
31
+ }
32
+
33
+ // Just resolve the path and validate it exists
34
+ const resolvedRoot = path.resolve(rootPath);
35
+ const stats = await fs.stat(resolvedRoot);
36
+
37
+ if (!stats.isDirectory()) {
38
+ throw new Error(`Path is not a directory: ${resolvedRoot}`);
39
+ }
40
+
41
+ console.log(chalk.gray(`Scanning directory: ${resolvedRoot}`));
42
+
43
+ const filesByExtension = {};
44
+ const scannedFiles = new Set(); // Prevent duplicates
45
+ const scanContext = {
46
+ errors: scanErrors,
47
+ warnings: scanWarnings,
48
+ skippedDirectories: 0,
49
+ skippedFiles: 0,
50
+ processedFiles: 0
51
+ };
52
+
53
+ await this.walkDirectory(resolvedRoot, resolvedRoot, filesByExtension, scannedFiles, scanContext);
54
+
55
+ // Sort files within each extension group
56
+ Object.keys(filesByExtension).forEach(ext => {
57
+ filesByExtension[ext].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
58
+ });
59
+
60
+ // Report scan summary with warnings/errors
61
+ this.reportScanIssues(scanContext);
62
+
63
+ return filesByExtension;
64
+ } catch (error) {
65
+ if (error.code === 'ENOENT') {
66
+ throw new Error(`Directory does not exist: ${rootPath}`);
67
+ } else if (error.code === 'EACCES') {
68
+ throw new Error(`Permission denied accessing directory: ${rootPath}`);
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Recursively walk through directory structure
76
+ * @param {string} currentPath - Current directory being processed
77
+ * @param {string} rootPath - Original root path for relative path calculation
78
+ * @param {object} filesByExtension - Accumulator object for results
79
+ * @param {Set} scannedFiles - Set to track processed files and avoid duplicates
80
+ * @param {object} scanContext - Context object to track scan statistics
81
+ */
82
+ async walkDirectory(currentPath, rootPath, filesByExtension, scannedFiles, scanContext) {
83
+ try {
84
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
85
+
86
+ for (const entry of entries) {
87
+ const fullPath = path.join(currentPath, entry.name);
88
+ const relativePath = path.relative(rootPath, fullPath);
89
+
90
+ if (entry.isDirectory()) {
91
+ // Skip excluded directories and hidden directories (unless explicitly allowed)
92
+ if (this.shouldSkipDirectory(entry.name, relativePath)) {
93
+ scanContext.skippedDirectories++;
94
+ continue;
95
+ }
96
+
97
+ // Recursively scan subdirectory
98
+ await this.walkDirectory(fullPath, rootPath, filesByExtension, scannedFiles, scanContext);
99
+ } else if (entry.isFile()) {
100
+ // Process file if it matches criteria
101
+ await this.processFile(fullPath, rootPath, filesByExtension, scannedFiles, scanContext);
102
+ } else if (entry.isSymbolicLink()) {
103
+ // Handle symbolic links with caution
104
+ scanContext.warnings.push(`Skipped symbolic link: ${relativePath}`);
105
+ }
106
+ // Skip other special files (devices, sockets, etc.)
107
+ }
108
+ } catch (error) {
109
+ // Track errors in context for better reporting
110
+ const relativePath = path.relative(rootPath, currentPath);
111
+
112
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
113
+ scanContext.errors.push(`Permission denied: ${relativePath}`);
114
+ } else if (error.code === 'ENOENT') {
115
+ scanContext.warnings.push(`Directory no longer exists: ${relativePath}`);
116
+ } else if (error.code === 'ENOTDIR') {
117
+ scanContext.warnings.push(`Path is not a directory: ${relativePath}`);
118
+ } else {
119
+ scanContext.errors.push(`Cannot read directory ${relativePath}: ${error.message}`);
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Process a single file and add it to results if it matches criteria
126
+ * @param {string} fullPath - Absolute path to the file
127
+ * @param {string} rootPath - Root path for relative calculation
128
+ * @param {object} filesByExtension - Results accumulator
129
+ * @param {Set} scannedFiles - Set of already processed files
130
+ * @param {object} scanContext - Context object to track scan statistics
131
+ */
132
+ async processFile(fullPath, rootPath, filesByExtension, scannedFiles, scanContext) {
133
+ try {
134
+ const relativePath = path.relative(rootPath, fullPath);
135
+
136
+ // Avoid processing the same file twice (in case of symlinks)
137
+ if (scannedFiles.has(fullPath)) {
138
+ return;
139
+ }
140
+ scannedFiles.add(fullPath);
141
+
142
+ const extension = path.extname(relativePath).toLowerCase();
143
+
144
+ // Skip files without extensions or not in allowed list
145
+ if (!extension || !this.allowedExtensions.has(extension)) {
146
+ scanContext.skippedFiles++;
147
+ return;
148
+ }
149
+
150
+ // Skip hidden files (starting with .) unless explicitly needed
151
+ const fileName = path.basename(relativePath);
152
+ if (fileName.startsWith('.') && !this.isAllowedHiddenFile(fileName)) {
153
+ scanContext.skippedFiles++;
154
+ return;
155
+ }
156
+
157
+ // Check if file should be excluded by pattern (e.g., *-lock.json)
158
+ if (this.shouldExcludeFile(fileName)) {
159
+ scanContext.skippedFiles++;
160
+ return;
161
+ }
162
+
163
+ // Verify file is readable
164
+ const stats = await fs.stat(fullPath);
165
+ if (!stats.isFile()) {
166
+ scanContext.warnings.push(`Skipped non-regular file: ${relativePath}`);
167
+ return;
168
+ }
169
+
170
+ // Check file size limits
171
+ const MAX_INDIVIDUAL_FILE_SIZE = 100 * 1024 * 1024; // 100MB per file
172
+ if (stats.size > MAX_INDIVIDUAL_FILE_SIZE) {
173
+ scanContext.warnings.push(`Skipped large file (${Math.round(stats.size / 1024 / 1024)}MB): ${relativePath}`);
174
+ scanContext.skippedFiles++;
175
+ return;
176
+ }
177
+
178
+ // Add to results
179
+ if (!filesByExtension[extension]) {
180
+ filesByExtension[extension] = [];
181
+ }
182
+
183
+ filesByExtension[extension].push({
184
+ relativePath: relativePath.replace(/\\/g, '/'), // Normalize path separators
185
+ absolutePath: fullPath,
186
+ size: stats.size,
187
+ modified: stats.mtime
188
+ });
189
+
190
+ scanContext.processedFiles++;
191
+
192
+ } catch (error) {
193
+ // Handle file processing errors with appropriate context
194
+ const relativePath = path.relative(rootPath, fullPath);
195
+
196
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
197
+ scanContext.errors.push(`Permission denied: ${relativePath}`);
198
+ } else if (error.code === 'ENOENT') {
199
+ // File might have been deleted during scan
200
+ scanContext.warnings.push(`File no longer exists: ${relativePath}`);
201
+ } else if (error.code === 'EISDIR') {
202
+ scanContext.warnings.push(`Path is a directory, not a file: ${relativePath}`);
203
+ } else {
204
+ scanContext.errors.push(`Cannot process file ${relativePath}: ${error.message}`);
205
+ }
206
+
207
+ scanContext.skippedFiles++;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Determine if a directory should be skipped
213
+ * @param {string} dirName - Directory name
214
+ * @param {string} relativePath - Relative path from root
215
+ * @returns {boolean} True if directory should be skipped
216
+ */
217
+ shouldSkipDirectory(dirName, relativePath) {
218
+ // Skip directories in exclude list
219
+ if (this.excludeDirs.has(dirName)) {
220
+ return true;
221
+ }
222
+
223
+ // Skip hidden directories (starting with .) unless explicitly allowed
224
+ if (dirName.startsWith('.') && !this.isAllowedHiddenDirectory(dirName)) {
225
+ return true;
226
+ }
227
+
228
+ // Skip common build/cache directories that might not be in exclude list
229
+ const commonSkipDirs = new Set([
230
+ 'tmp', 'temp', 'cache', '.cache', 'logs', '.logs',
231
+ 'bower_components', 'vendor', '.vendor'
232
+ ]);
233
+
234
+ if (commonSkipDirs.has(dirName.toLowerCase())) {
235
+ return true;
236
+ }
237
+
238
+ return false;
239
+ }
240
+
241
+ /**
242
+ * Check if a file should be excluded based on patterns
243
+ * @param {string} fileName - File name to check
244
+ * @returns {boolean} True if file should be excluded
245
+ */
246
+ shouldExcludeFile(fileName) {
247
+ for (const pattern of this.excludeFiles) {
248
+ if (this.matchesPattern(fileName, pattern)) {
249
+ return true;
250
+ }
251
+ }
252
+ return false;
253
+ }
254
+
255
+ /**
256
+ * Simple glob pattern matching
257
+ * @param {string} fileName - File name to test
258
+ * @param {string} pattern - Pattern to match (supports * wildcards)
259
+ * @returns {boolean} True if pattern matches
260
+ */
261
+ matchesPattern(fileName, pattern) {
262
+ // Exact match
263
+ if (pattern === fileName) {
264
+ return true;
265
+ }
266
+
267
+ // Convert glob pattern to regex
268
+ const regexPattern = pattern
269
+ .replace(/\./g, '\\.') // Escape dots
270
+ .replace(/\*/g, '.*'); // Convert * to .*
271
+
272
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
273
+ return regex.test(fileName);
274
+ }
275
+
276
+ /**
277
+ * Check if a hidden file should be included
278
+ * @param {string} fileName - File name
279
+ * @returns {boolean} True if file should be included
280
+ */
281
+ isAllowedHiddenFile(fileName) {
282
+ const allowedHiddenFiles = new Set([
283
+ '.gitignore', '.gitattributes', '.editorconfig',
284
+ '.eslintrc.js', '.eslintrc.json', '.prettierrc',
285
+ '.env.example', '.htaccess'
286
+ ]);
287
+
288
+ return allowedHiddenFiles.has(fileName);
289
+ }
290
+
291
+ /**
292
+ * Check if a hidden directory should be included
293
+ * @param {string} dirName - Directory name
294
+ * @returns {boolean} True if directory should be included
295
+ */
296
+ isAllowedHiddenDirectory(dirName) {
297
+ const allowedHiddenDirs = new Set([
298
+ '.github', '.gitlab', '.circleci'
299
+ ]);
300
+
301
+ return allowedHiddenDirs.has(dirName);
302
+ }
303
+
304
+ /**
305
+ * Get file extension descriptions for user display
306
+ * @param {object} filesByExtension - Files grouped by extension
307
+ * @returns {Array} Array of extension info objects
308
+ */
309
+ getExtensionInfo(filesByExtension) {
310
+ const extensionDescriptions = {
311
+ '.js': 'JavaScript',
312
+ '.ts': 'TypeScript',
313
+ '.jsx': 'React JSX',
314
+ '.tsx': 'TypeScript JSX',
315
+ '.json': 'JSON',
316
+ '.xml': 'XML',
317
+ '.html': 'HTML',
318
+ '.css': 'CSS',
319
+ '.scss': 'SCSS',
320
+ '.sass': 'Sass',
321
+ '.md': 'Markdown',
322
+ '.txt': 'Text',
323
+ '.py': 'Python',
324
+ '.java': 'Java',
325
+ '.cs': 'C#',
326
+ '.cpp': 'C++',
327
+ '.c': 'C',
328
+ '.h': 'Header',
329
+ '.yaml': 'YAML',
330
+ '.yml': 'YAML',
331
+ '.sh': 'Shell Script',
332
+ '.bat': 'Batch File',
333
+ '.ps1': 'PowerShell',
334
+ '.php': 'PHP',
335
+ '.rb': 'Ruby',
336
+ '.go': 'Go',
337
+ '.rs': 'Rust',
338
+ '.swift': 'Swift',
339
+ '.kt': 'Kotlin',
340
+ '.scala': 'Scala',
341
+ '.vue': 'Vue.js',
342
+ '.svelte': 'Svelte',
343
+ '.dockerfile': 'Dockerfile',
344
+ '.sql': 'SQL',
345
+ '.graphql': 'GraphQL'
346
+ };
347
+
348
+ return Object.keys(filesByExtension)
349
+ .sort()
350
+ .map(ext => ({
351
+ extension: ext,
352
+ description: extensionDescriptions[ext] || 'Unknown',
353
+ count: filesByExtension[ext].length,
354
+ files: filesByExtension[ext]
355
+ }));
356
+ }
357
+
358
+ /**
359
+ * Calculate total statistics for scanned files
360
+ * @param {object} filesByExtension - Files grouped by extension
361
+ * @returns {object} Statistics object
362
+ */
363
+ calculateStatistics(filesByExtension) {
364
+ let totalFiles = 0;
365
+ let totalSize = 0;
366
+ const extensionCount = Object.keys(filesByExtension).length;
367
+
368
+ Object.values(filesByExtension).forEach(files => {
369
+ totalFiles += files.length;
370
+ totalSize += files.reduce((sum, file) => sum + file.size, 0);
371
+ });
372
+
373
+ return {
374
+ totalFiles,
375
+ totalSize,
376
+ extensionCount,
377
+ averageFileSize: totalFiles > 0 ? Math.round(totalSize / totalFiles) : 0,
378
+ totalSizeFormatted: this.formatFileSize(totalSize)
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Format file size in human readable format
384
+ * @param {number} bytes - Size in bytes
385
+ * @returns {string} Formatted size string
386
+ */
387
+ formatFileSize(bytes) {
388
+ const units = ['B', 'KB', 'MB', 'GB'];
389
+ let size = bytes;
390
+ let unitIndex = 0;
391
+
392
+ while (size >= 1024 && unitIndex < units.length - 1) {
393
+ size /= 1024;
394
+ unitIndex++;
395
+ }
396
+
397
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
398
+ }
399
+
400
+ /**
401
+ * Report scan issues and statistics
402
+ * @param {object} scanContext - Context object with scan statistics
403
+ */
404
+ reportScanIssues(scanContext) {
405
+ const { errors, warnings, skippedDirectories, skippedFiles, processedFiles } = scanContext;
406
+
407
+ // Report critical errors
408
+ if (errors.length > 0) {
409
+ console.log(chalk.red(`\n⚠️ ${errors.length} scan error(s):`));
410
+ errors.slice(0, 5).forEach(error => {
411
+ console.log(chalk.red(` • ${error}`));
412
+ });
413
+ if (errors.length > 5) {
414
+ console.log(chalk.gray(` ... and ${errors.length - 5} more errors`));
415
+ }
416
+ }
417
+
418
+ // Report warnings (less critical)
419
+ if (warnings.length > 0 && process.env.NODE_ENV === 'development') {
420
+ console.log(chalk.yellow(`\n⚠️ ${warnings.length} scan warning(s):`));
421
+ warnings.slice(0, 3).forEach(warning => {
422
+ console.log(chalk.yellow(` • ${warning}`));
423
+ });
424
+ if (warnings.length > 3) {
425
+ console.log(chalk.gray(` ... and ${warnings.length - 3} more warnings`));
426
+ }
427
+ }
428
+
429
+ // Report summary statistics
430
+ const totalIssues = errors.length + warnings.length;
431
+ if (skippedFiles > 0 || skippedDirectories > 0 || totalIssues > 0) {
432
+ console.log(chalk.gray(`\n📊 Scan Statistics:`));
433
+ console.log(chalk.gray(` Processed: ${processedFiles} files`));
434
+ if (skippedFiles > 0) {
435
+ console.log(chalk.gray(` Skipped: ${skippedFiles} files`));
436
+ }
437
+ if (skippedDirectories > 0) {
438
+ console.log(chalk.gray(` Skipped: ${skippedDirectories} directories`));
439
+ }
440
+ if (totalIssues > 0) {
441
+ console.log(chalk.gray(` Issues: ${errors.length} errors, ${warnings.length} warnings`));
442
+ }
443
+ }
444
+
445
+ // Warn if scan completeness is compromised
446
+ if (errors.length > 0) {
447
+ console.log(chalk.yellow(`\n⚠️ WARNING: Scan may be incomplete due to ${errors.length} access errors.`));
448
+ console.log(chalk.gray(' Some files or directories could not be accessed.'));
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Display scan results summary
454
+ * @param {object} filesByExtension - Files grouped by extension
455
+ */
456
+ displayScanSummary(filesByExtension) {
457
+ const stats = this.calculateStatistics(filesByExtension);
458
+ const extensions = Object.keys(filesByExtension).sort();
459
+
460
+ console.log(chalk.cyan('\n📊 Scan Summary:'));
461
+ console.log(chalk.gray(` Extensions found: ${extensions.join(', ')}`));
462
+ console.log(chalk.gray(` Total files: ${stats.totalFiles}`));
463
+ console.log(chalk.gray(` Total size: ${stats.totalSizeFormatted}`));
464
+ console.log();
465
+ }
466
+ }
467
+
330
468
  export default Scanner;