/* * Copyright (c) 2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; // Define file type extensions const FILE_TYPES: { [key: string]: string[] } = { 'C/C++': ['.c', '.h', '.cpp', '.hpp'], JavaScript: ['.js'], TypeScript: ['.ts'], JSON: ['.json', '.json5'], XML: ['.xml'], ArkTS: ['.ets'], 'ArkTS Test': ['.test.ets'] }; // Comment regex patterns by file type const COMMENT_REGEX1: { [key: string]: RegExp } = { 'C/C++': /\/\/.*/g, JavaScript: /\/\/.*/g, TypeScript: /\/\/.*/g, ArkTS: /\/\/.*/g, 'ArkTS Test': /\/\/.*/g, XML: //g }; const COMMENT_REGEX2: { [key: string]: RegExp } = { 'C/C++': /\/\*.*?\*\//gs, JavaScript: /\/\*.*?\*\//gs, TypeScript: /\/\*.*?\*\//gs, ArkTS: /\/\*.*?\*\//gs, 'ArkTS Test': /\/\*.*?\*\//gs }; /** * Remove comments from file content */ function removeComments(content: string, fileType: string): string { if (COMMENT_REGEX1[fileType]) { content = content.replace(COMMENT_REGEX1[fileType], ''); } if (COMMENT_REGEX2[fileType]) { content = content.replace(COMMENT_REGEX2[fileType], ''); } return content; } /** * Count valid lines of code (excluding comments and empty lines) */ async function countLines(filePath: string, fileType: string): Promise { try { const content = await promisify(fs.readFile)(filePath, 'utf-8'); const cleanedContent = removeComments(content, fileType); const lines = cleanedContent.split('\n'); // Filter out empty lines const validLines = lines.filter((line) => { return line.trim(); }); return validLines.length; } catch (error) { console.error(`Error reading ${filePath}: ${error}`); return 0; } } /** * Merge multiple result objects */ function mergeAllResults(results: { [key: string]: { fileCount: number; lineCount: number } }[]): { [key: string]: { fileCount: number; lineCount: number }; } { const combined: { [key: string]: { fileCount: number; lineCount: number } } = {}; for (const result of results) { for (const [type, counts] of Object.entries(result)) { if (!combined[type]) { combined[type] = { fileCount: 0, lineCount: 0 }; } combined[type].fileCount += counts.fileCount; combined[type].lineCount += counts.lineCount; } } return combined; } /** * Process directory entries recursively */ async function walkDir(dir: string): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { try { const entries = await promisify(fs.readdir)(dir, { withFileTypes: true }); const fileResults = await processFiles(dir, entries); const dirResults = await processDirs(dir, entries); return mergeAllResults([fileResults, dirResults]); } catch (error) { console.error(`Error reading ${dir}: ${error}`); return {}; } } /** * Process files in a directory */ async function processFiles( dir: string, entries: fs.Dirent[] ): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { const fileResults = await Promise.all( entries. filter((entry) => { return entry.isFile(); }). map(async(entry) => { return processFileEntry(dir, entry); }) ); return mergeAllResults(fileResults); } /** * Process a single file entry */ async function processFileEntry( dir: string, entry: fs.Dirent ): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { const fullPath = path.join(dir, entry.name); for (const fileType in FILE_TYPES) { const extensions = FILE_TYPES[fileType]; const ext = path.extname(entry.name); if (extensions.includes(ext)) { const lines = await countLines(fullPath, fileType); return { [fileType]: { fileCount: 1, lineCount: lines } }; } } return {}; } /** * Process subdirectories recursively */ async function processDirs( dir: string, entries: fs.Dirent[] ): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { const dirEntries = entries.filter((entry) => { return entry.isDirectory(); }); const dirResults = await Promise.all( dirEntries.map((entry) => { return walkDir(path.join(dir, entry.name)); }) ); return mergeAllResults(dirResults); } /** * Analyze directory and count files/lines by type */ async function analyzeDirectory( directory: string ): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { // Initialize results const results: { [key: string]: { fileCount: number; lineCount: number } } = {}; for (const fileType in FILE_TYPES) { results[fileType] = { fileCount: 0, lineCount: 0 }; } const finalResults = await walkDir(directory); Object.assign(results, finalResults); return results; } /** * Main export function */ export async function countFiles( directory: string ): Promise<{ [key: string]: { fileCount: number; lineCount: number } }> { try { const stats = await promisify(fs.stat)(directory); if (stats.isDirectory()) { return await analyzeDirectory(directory); } console.error('The directory is invalid!'); return {}; } catch (error) { console.error('Read directory failed', error); return {}; } }