1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import fs from 'fs'; 17import path from 'path'; 18import { promisify } from 'util'; 19 20// Define file type extensions 21const FILE_TYPES: { [key: string]: string[] } = { 22 'C/C++': ['.c', '.h', '.cpp', '.hpp'], 23 JavaScript: ['.js'], 24 TypeScript: ['.ts'], 25 JSON: ['.json', '.json5'], 26 XML: ['.xml'], 27 ArkTS: ['.ets'], 28 'ArkTS Test': ['.test.ets'] 29}; 30 31// Comment regex patterns by file type 32const COMMENT_REGEX1: { [key: string]: RegExp } = { 33 'C/C++': /\/\/.*/g, 34 JavaScript: /\/\/.*/g, 35 TypeScript: /\/\/.*/g, 36 ArkTS: /\/\/.*/g, 37 'ArkTS Test': /\/\/.*/g, 38 XML: /<!--.*?-->/g 39}; 40 41const COMMENT_REGEX2: { [key: string]: RegExp } = { 42 'C/C++': /\/\*.*?\*\//gs, 43 JavaScript: /\/\*.*?\*\//gs, 44 TypeScript: /\/\*.*?\*\//gs, 45 ArkTS: /\/\*.*?\*\//gs, 46 'ArkTS Test': /\/\*.*?\*\//gs 47}; 48 49/** 50 * Remove comments from file content 51 */ 52function removeComments(content: string, fileType: string): string { 53 if (COMMENT_REGEX1[fileType]) { 54 content = content.replace(COMMENT_REGEX1[fileType], ''); 55 } 56 if (COMMENT_REGEX2[fileType]) { 57 content = content.replace(COMMENT_REGEX2[fileType], ''); 58 } 59 return content; 60} 61 62/** 63 * Count valid lines of code (excluding comments and empty lines) 64 */ 65async function countLines(filePath: string, fileType: string): Promise<number> { 66 try { 67 const content = await promisify(fs.readFile)(filePath, 'utf-8'); 68 const cleanedContent = removeComments(content, fileType); 69 const lines = cleanedContent.split('\n'); 70 71 // Filter out empty lines 72 const validLines = lines.filter((line) => { 73 return line.trim(); 74 }); 75 return validLines.length; 76 } catch (error) { 77 console.error(`Error reading ${filePath}: ${error}`); 78 return 0; 79 } 80} 81 82/** 83 * Merge multiple result objects 84 */ 85function mergeAllResults(results: { [key: string]: { fileCount: number; lineCount: number } }[]): { 86 [key: string]: { fileCount: number; lineCount: number }; 87} { 88 const combined: { [key: string]: { fileCount: number; lineCount: number } } = {}; 89 90 for (const result of results) { 91 for (const [type, counts] of Object.entries(result)) { 92 if (!combined[type]) { 93 combined[type] = { fileCount: 0, lineCount: 0 }; 94 } 95 combined[type].fileCount += counts.fileCount; 96 combined[type].lineCount += counts.lineCount; 97 } 98 } 99 100 return combined; 101} 102 103/** 104 * Process directory entries recursively 105 */ 106async function walkDir(dir: string): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { 107 try { 108 const entries = await promisify(fs.readdir)(dir, { withFileTypes: true }); 109 const fileResults = await processFiles(dir, entries); 110 const dirResults = await processDirs(dir, entries); 111 return mergeAllResults([fileResults, dirResults]); 112 } catch (error) { 113 console.error(`Error reading ${dir}: ${error}`); 114 return {}; 115 } 116} 117 118/** 119 * Process files in a directory 120 */ 121async function processFiles( 122 dir: string, 123 entries: fs.Dirent[] 124): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { 125 const fileResults = await Promise.all( 126 entries. 127 filter((entry) => { 128 return entry.isFile(); 129 }). 130 map(async(entry) => { 131 return processFileEntry(dir, entry); 132 }) 133 ); 134 return mergeAllResults(fileResults); 135} 136 137/** 138 * Process a single file entry 139 */ 140async function processFileEntry( 141 dir: string, 142 entry: fs.Dirent 143): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { 144 const fullPath = path.join(dir, entry.name); 145 146 for (const fileType in FILE_TYPES) { 147 const extensions = FILE_TYPES[fileType]; 148 const ext = path.extname(entry.name); 149 150 if (extensions.includes(ext)) { 151 const lines = await countLines(fullPath, fileType); 152 return { 153 [fileType]: { 154 fileCount: 1, 155 lineCount: lines 156 } 157 }; 158 } 159 } 160 161 return {}; 162} 163 164/** 165 * Process subdirectories recursively 166 */ 167async function processDirs( 168 dir: string, 169 entries: fs.Dirent[] 170): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { 171 const dirEntries = entries.filter((entry) => { 172 return entry.isDirectory(); 173 }); 174 const dirResults = await Promise.all( 175 dirEntries.map((entry) => { 176 return walkDir(path.join(dir, entry.name)); 177 }) 178 ); 179 return mergeAllResults(dirResults); 180} 181 182/** 183 * Analyze directory and count files/lines by type 184 */ 185async function analyzeDirectory( 186 directory: string 187): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { 188 // Initialize results 189 const results: { [key: string]: { fileCount: number; lineCount: number } } = {}; 190 for (const fileType in FILE_TYPES) { 191 results[fileType] = { fileCount: 0, lineCount: 0 }; 192 } 193 194 const finalResults = await walkDir(directory); 195 Object.assign(results, finalResults); 196 return results; 197} 198 199/** 200 * Main export function 201 */ 202export async function countFiles( 203 directory: string 204): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { 205 try { 206 const stats = await promisify(fs.stat)(directory); 207 if (stats.isDirectory()) { 208 return await analyzeDirectory(directory); 209 } 210 console.error('The directory is invalid!'); 211 return {}; 212 } catch (error) { 213 console.error('Read directory failed', error); 214 return {}; 215 } 216} 217